SlideShare una empresa de Scribd logo
Introducci´on a la programaci´on
con C
Andr´es Marzal Isabel Gracia
Departamento de Lenguajes y Sistemas Inform´aticos
Universitat Jaume I
Obra distribuida con licencia Creative Commons
Esta obra se distribuye con licencia Creative Commons en su modalidad ((Reconocimiento-No
Comercial-Sin obras derivadas 2.5 Espa˜na)).
Usted es libre de copiar, distribuir y comunicar p´ublicamente la obra bajo las condiciones
siguientes:
Reconocimiento. Debe reconocer los cr´editos de la obra de la manera especificada
por el autor o el licenciador (pero no de una manera que sugiera que tiene su apoyo
o apoyan el uso que hace de su obra).
No comercial. No puede utilizar esta obra para fines comerciales.
Sin obras derivadas. No se puede alterar, transformar o generar una obra derivada a
partir de esta obra.
Este texto es un resumen de la licencia. El texto completo de la licencia se encuentra en
http://creativecommons.org/licenses/by-nc-nd/2.5/es/legalcode.es.
ii Introducci´on a la Programaci´on con C
´Indice general
1. Introducci´on a C 1
1.1. C es un lenguaje compilado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2. Traduciendo de Python a C: una gu´ıa r´apida . . . . . . . . . . . . . . . . . . . . 5
1.3. Estructura t´ıpica de un programa C . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.4. C es un lenguaje de formato libre . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.5. Hay dos tipos de comentario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.6. Valores literales en C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
1.6.1. Enteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
1.6.2. Flotantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
1.6.3. Cadenas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
1.7. C tiene un rico juego de tipos escalares . . . . . . . . . . . . . . . . . . . . . . . . 23
1.7.1. El tipo int . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
1.7.2. El tipo unsigned int . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
1.7.3. El tipo float . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.7.4. El tipo char . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.7.5. El tipo unsigned char . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.8. Se debe declarar el tipo de toda variable antes de usarla . . . . . . . . . . . . . . 24
1.8.1. Identificadores v´alidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.8.2. Sentencias de declaraci´on . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.8.3. Declaraci´on con inicializaci´on . . . . . . . . . . . . . . . . . . . . . . . . . 26
1.9. Salida por pantalla . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
1.9.1. Marcas de formato para la impresi´on de valores con printf . . . . . . . . . 27
1.10. Variables y direcciones de memoria . . . . . . . . . . . . . . . . . . . . . . . . . . 31
1.11. Entrada por teclado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
1.12. Expresiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
1.13. Conversi´on impl´ıcita y expl´ıcita de tipos . . . . . . . . . . . . . . . . . . . . . . . 41
1.14. Las directivas y el preprocesador . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
1.15. Constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
1.15.1. Definidas con la directiva define . . . . . . . . . . . . . . . . . . . . . . . 44
1.15.2. Definidas con el adjetivo const . . . . . . . . . . . . . . . . . . . . . . . . 44
1.15.3. Con tipos enumerados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
1.16. Las bibliotecas (m´odulos) se importan con #include . . . . . . . . . . . . . . . . 47
1.16.1. La biblioteca matem´atica . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
1.17. Estructuras de control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
1.17.1. Estructuras de control condicionales . . . . . . . . . . . . . . . . . . . . . 49
1.17.2. Estructuras de control iterativas . . . . . . . . . . . . . . . . . . . . . . . 54
1.17.3. Sentencias para alterar el flujo iterativo . . . . . . . . . . . . . . . . . . . 59
2. Estructuras de datos en C: vectores est´aticos y registros 63
2.1. Vectores est´aticos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
2.1.1. Declaraci´on de vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
2.1.2. Inicializaci´on de los vectores . . . . . . . . . . . . . . . . . . . . . . . . . . 64
2.1.3. Un programa de ejemplo: la criba de Erat´ostenes . . . . . . . . . . . . . . 65
2.1.4. Otro programa de ejemplo: estad´ısticas . . . . . . . . . . . . . . . . . . . 68
2.1.5. Otro programa de ejemplo: una calculadora para polinomios . . . . . . . . 77
2.1.6. Disposici´on de los vectores en memoria . . . . . . . . . . . . . . . . . . . . 83
2.1.7. Algunos problemas de C: accesos il´ıcitos a memoria . . . . . . . . . . . . 87
Introducci´on a la Programaci´on con C i
´INDICE GENERAL
2.1.8. Asignaci´on y copia de vectores . . . . . . . . . . . . . . . . . . . . . . . . 88
2.1.9. Comparaci´on de vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
2.2. Cadenas est´aticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
2.2.1. Declaraci´on de cadenas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
2.2.2. Representaci´on de las cadenas en memoria . . . . . . . . . . . . . . . . . . 91
2.2.3. Entrada/salida de cadenas . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
2.2.4. Asignaci´on y copia de cadenas . . . . . . . . . . . . . . . . . . . . . . . . 97
2.2.5. Longitud de una cadena . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
2.2.6. Concatenaci´on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
2.2.7. Comparaci´on de cadenas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
2.2.8. Funciones ´utiles para manejar caracteres . . . . . . . . . . . . . . . . . . . 107
2.2.9. Escritura en cadenas: sprintf . . . . . . . . . . . . . . . . . . . . . . . . . 108
2.2.10. Un programa de ejemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
2.3. Vectores multidimensionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
2.3.1. Sobre la disposici´on de los vectores multidimensionales en memoria . . . . 110
2.3.2. Un ejemplo: c´alculo matricial . . . . . . . . . . . . . . . . . . . . . . . . . 111
2.3.3. Vectores de cadenas, matrices de caracteres . . . . . . . . . . . . . . . . . 117
2.4. Registros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
2.4.1. Un ejemplo: registros para almacenar vectores de talla variable (pero
acotada) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
2.4.2. Un ejemplo: rectas de regresi´on para una serie de puntos en el plano . . . 129
2.4.3. Otro ejemplo: gesti´on de una coleci´on de CDs . . . . . . . . . . . . . . . . 132
2.5. Definici´on de nuevos tipos de datos . . . . . . . . . . . . . . . . . . . . . . . . . . 136
3. Funciones 139
3.1. Definici´on de funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
3.2. Variables locales y globales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
3.2.1. Variables locales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
3.2.2. Variables globales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
3.3. Funciones sin par´ametros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
3.4. Procedimientos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
3.5. Paso de par´ametros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
3.5.1. Par´ametros escalares: paso por valor . . . . . . . . . . . . . . . . . . . . . 149
3.5.2. Organizaci´on de la memoria: la pila de llamadas a funci´on . . . . . . . . . 149
3.5.3. Vectores de longitud variable . . . . . . . . . . . . . . . . . . . . . . . . . 155
3.5.4. Par´ametros vectoriales: paso por referencia . . . . . . . . . . . . . . . . . 155
3.5.5. Par´ametros escalares: paso por referencia mediante punteros . . . . . . . . 161
3.5.6. Paso de registros a funciones . . . . . . . . . . . . . . . . . . . . . . . . . 166
3.5.7. Paso de matrices y otros vectores multidimensionales . . . . . . . . . . . . 169
3.5.8. Tipos de retorno v´alidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
3.5.9. Un ejercicio pr´actico: miniGalaxis . . . . . . . . . . . . . . . . . . . . . . 173
3.6. Recursi´on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
3.6.1. Un m´etodo recursivo de ordenaci´on: mergesort . . . . . . . . . . . . . . . 191
3.6.2. Recursi´on indirecta y declaraci´on anticipada . . . . . . . . . . . . . . . . . 197
3.7. Macros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198
3.8. Otras cuestiones acerca de las funciones . . . . . . . . . . . . . . . . . . . . . . . 201
3.8.1. Funciones inline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
3.8.2. Variables locales static . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
3.8.3. Paso de funciones como par´ametros . . . . . . . . . . . . . . . . . . . . . 203
3.9. M´odulos, bibliotecas y unidades de compilaci´on . . . . . . . . . . . . . . . . . . . 205
3.9.1. Declaraci´on de prototipos en cabeceras . . . . . . . . . . . . . . . . . . . . 207
3.9.2. Declaraci´on de variables en cabeceras . . . . . . . . . . . . . . . . . . . . 209
3.9.3. Declaraci´on de registros en cabeceras . . . . . . . . . . . . . . . . . . . . . 210
ii Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 0 ´INDICE GENERAL
4. Estructuras de datos: memoria din´amica 213
4.1. Vectores din´amicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
4.1.1. malloc, free y NULL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
4.1.2. Algunos ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217
4.1.3. Cadenas din´amicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229
4.2. Matrices din´amicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230
4.2.1. Gesti´on de memoria para matrices din´amicas . . . . . . . . . . . . . . . . 230
4.2.2. Definici´on de un tipo ((matriz din´amica)) y de funciones para su gesti´on . 234
4.3. M´as all´a de las matrices din´amicas . . . . . . . . . . . . . . . . . . . . . . . . . . 238
4.3.1. Vectores de vectores de tallas arbitrarias . . . . . . . . . . . . . . . . . . . 238
4.3.2. Arreglos n-dimensionales . . . . . . . . . . . . . . . . . . . . . . . . . . . 242
4.4. Redimensionamiento de la reserva de memoria . . . . . . . . . . . . . . . . . . . 243
4.4.1. Una aplicaci´on: una agenda telef´onica . . . . . . . . . . . . . . . . . . . . 251
4.5. Introducci´on a la gesti´on de registros enlazados . . . . . . . . . . . . . . . . . . . 256
4.5.1. Definici´on y creaci´on de la lista . . . . . . . . . . . . . . . . . . . . . . . . 258
4.5.2. Adici´on de nodos (por cabeza) . . . . . . . . . . . . . . . . . . . . . . . . 259
4.5.3. Adici´on de un nodo (por cola) . . . . . . . . . . . . . . . . . . . . . . . . 261
4.5.4. Borrado de la cabeza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
4.6. Listas con enlace simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
4.6.1. Creaci´on de lista vac´ıa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268
4.6.2. ¿Lista vac´ıa? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268
4.6.3. Inserci´on por cabeza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268
4.6.4. Longitud de una lista . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271
4.6.5. Impresi´on en pantalla . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272
4.6.6. Inserci´on por cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273
4.6.7. Borrado de la cabeza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
4.6.8. Borrado de la cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
4.6.9. B´usqueda de un elemento . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
4.6.10. Borrado del primer nodo con un valor determinado . . . . . . . . . . . . . 280
4.6.11. Borrado de todos los nodos con un valor dado . . . . . . . . . . . . . . . . 282
4.6.12. Inserci´on en una posici´on dada . . . . . . . . . . . . . . . . . . . . . . . . 283
4.6.13. Inserci´on ordenada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285
4.6.14. Concatenaci´on de dos listas . . . . . . . . . . . . . . . . . . . . . . . . . . 286
4.6.15. Borrado de la lista completa . . . . . . . . . . . . . . . . . . . . . . . . . . 287
4.6.16. Juntando las piezas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 287
4.7. Listas simples con punteros a cabeza y cola . . . . . . . . . . . . . . . . . . . . . 294
4.7.1. Creaci´on de lista vac´ıa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 295
4.7.2. Inserci´on de nodo en cabeza . . . . . . . . . . . . . . . . . . . . . . . . . . 295
4.7.3. Inserci´on de nodo en cola . . . . . . . . . . . . . . . . . . . . . . . . . . . 296
4.7.4. Borrado de la cabeza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 297
4.7.5. Borrado de la cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 298
4.8. Listas con enlace doble . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300
4.8.1. Inserci´on por cabeza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300
4.8.2. Borrado de la cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 302
4.8.3. Inserci´on en una posici´on determinada . . . . . . . . . . . . . . . . . . . . 303
4.8.4. Borrado de la primera aparici´on de un elemento . . . . . . . . . . . . . . 305
4.9. Listas con enlace doble y puntero a cabeza y cola . . . . . . . . . . . . . . . . . . 307
4.10. Una gu´ıa para elegir listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 310
4.11. Una aplicaci´on: una base de datos para discos compactos . . . . . . . . . . . . . 311
4.12. Otras estructuras de datos con registros enlazados . . . . . . . . . . . . . . . . . 318
5. Ficheros 321
5.1. Ficheros de texto y ficheros binarios . . . . . . . . . . . . . . . . . . . . . . . . . 321
5.1.1. Representaci´on de la informaci´on en los ficheros de texto . . . . . . . . . . 321
5.1.2. Representaci´on de la informaci´on en los ficheros binarios . . . . . . . . . . 322
5.2. Ficheros de texto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 323
5.2.1. Abrir, leer/escribir, cerrar . . . . . . . . . . . . . . . . . . . . . . . . . . . 323
5.2.2. Aplicaciones: una agenda y un gestor de una colecci´on de discos compactos330
5.2.3. Los ((ficheros)) de consola . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
Introducci´on a la Programaci´on con C iii
´INDICE GENERAL
5.2.4. Un par de utilidades . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 342
5.3. Ficheros binarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 344
5.3.1. Abrir, leer/escribir, cerrar . . . . . . . . . . . . . . . . . . . . . . . . . . . 344
5.3.2. Acceso directo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 349
5.4. Errores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
A. Tipos b´asicos 355
A.1. Enteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355
A.1.1. Tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355
A.1.2. Literales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
A.1.3. Marcas de formato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
A.2. Flotantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
A.2.1. Tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
A.2.2. Literales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357
A.2.3. Marcas de formato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357
A.3. Caracteres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357
A.3.1. Literales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357
A.3.2. Marcas de formato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 358
A.4. Otros tipos b´asicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 358
A.4.1. El tipo booleano . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 358
A.4.2. Los tipos complejo e imaginario . . . . . . . . . . . . . . . . . . . . . . . . 358
A.5. Una reflexi´on acerca de la diversidad de tipos escalares . . . . . . . . . . . . . . . 358
B. La lectura de datos por teclado, paso a paso 359
B.1. La lectura de valores escalares con scanf . . . . . . . . . . . . . . . . . . . . . . . 359
B.2. La lectura de cadenas con scanf . . . . . . . . . . . . . . . . . . . . . . . . . . . 362
B.3. Un problema serio: la lectura alterna de cadenas con gets y de escalares con scanf 366
iv Introducci´on a la Programaci´on con C
Cap´ıtulo 1
Introducci´on a C
Hab´ıa un libro junto a Alicia, en la mesa; y mientras permanec´ıa sentada observando al
Rey Blanco [. . . ], pasaba las hojas para ver si encontraba alg´un trozo que poder leer:
((. . . Porque est´a todo en una lengua que no entiendo)), se dijo.
Estaba as´ı:
JERIG´ONDOR
Cocillabaeld´ıaylastovasagilimosas
giroscopabanybarrenabanenellarde.
Tododevirablesestabanlosburgovos,
ysilbramabanlasalecasrastas.
Durante un rato, estuvo contemplando esto perpleja; pero al final se le ocurri´o una
brillante idea. ¡Ah, ya s´e!, ¡es un libro del Espejo, naturalmente! Si lo pongo delante de
un espejo, las palabras se ver´an otra vez del derecho.
Lewis Carroll, Alicia a trav´es del espejo.
El lenguaje de programaci´on C es uno de los m´as utilizados (si no el que m´as) en la programaci´on
de sistemas software. Es similar a Python en muchos aspectos fundamentales: presenta las
mismas estructuras de control (selecci´on condicional, iteraci´on), permite trabajar con algunos
tipos de datos similares (enteros, flotantes, secuencias), hace posible definir y usar funciones,
etc. No obstante, en muchas otras cuestiones es un lenguaje muy diferente.
C presenta ciertas caracter´ısticas que permiten ejercer un elevado control sobre la eficiencia
de los programas, tanto en la velocidad de ejecuci´on como en el consumo de memoria, pero
a un precio: tenemos que proporcionar informaci´on expl´ıcita sobre gran cantidad de detalles,
por lo que generalmente resultan programas m´as largos y complicados que sus equivalentes en
Python, aumentando as´ı la probabilidad de que cometamos errores.
En este cap´ıtulo aprenderemos a realizar programas en C del mismo ((nivel)) que los que
sab´ıamos escribir en Python tras estudiar el cap´ıtulo 4 del primer volumen. Aprenderemos,
pues, a usar variables, expresiones, la entrada/salida, funciones definidas en ((m´odulos)) (que
en C se denominan bibliotecas) y estructuras de control. Lo ´unico que dejamos pendiente de
momento es el tratamiento de cadenas en C, que es sensiblemente diferente al que proporciona
Python.
Nada mejor que un ejemplo de programa en los dos lenguajes para que te lleves una primera
impresi´on de cu´an diferentes son Python y C. . . y cu´an semejantes. Estos dos programas, el
primero en Python y el segundo en C, calculan el valor de
b
i=a
√
i
para sendos valores enteros de a y b introducidos por el usuario y tales que 0 ≤ a ≤ b.
Introducci´on a la Programaci´on con C 1
Introducci´on a C
sumatorio.py sumatorio.py
1 from math import *
2
3 # Pedir l´ımites inferior y superior.
4 a = int(raw_input(’L´ımite inferior:’))
5 while a < 0:
6 print ’No puede ser negativo’
7 a = int(raw_input(’L´ımite inferior:’))
8
9 b = int(raw_input(’L´ımite superior:’))
10 while b < a:
11 print ’No puede ser menor que %d’ % a
12 b = int(raw_input(’L´ımite superior:’))
13
14 # Calcular el sumatorio de la ra´ız cuadrada de i para i entre a y b.
15 s = 0.0
16 for i in range(a, b+1):
17 s += sqrt(i)
18
19 # Mostrar el resultado.
20 print ’Sumatorio de ra´ıces’,
21 print ’de %d a %d: %f’ % (a, b, s)
sumatorio.c sumatorio.c
1 #include <stdio.h>
2 #include <math.h>
3
4 int main(void)
5 {
6 int a, b, i;
7 float s;
8
9 /* Pedir l´ımites inferior y superior. */
10 printf ("L´ımite inferior:");
11 scanf ("%d", &a);
12 while (a < 0) {
13 printf ("No puede ser negativon");
14 printf ("L´ımite inferior:");
15 scanf ("%d", &a);
16 }
17
18 printf ("L´ımite superior:");
19 scanf ("%d", &b);
20 while (b < a) {
21 printf ("No puede ser menor que %dn", a);
22 printf ("L´ımite superior:");
23 scanf ("%d", &b);
24 }
25
26 /* Calcular el sumatorio de la ra´ız cuadrada de i para i entre a y b. */
27 s = 0.0;
28 for (i = a; i <= b; i++) {
29 s += sqrt(i);
30 }
31
32 /* Mostrar el resultado. */
33 printf ("Sumatorio de ra´ıces ");
34 printf ("de %d a %d: %fn", a, b, s);
35
36 return 0;
37 }
En varios puntos de este cap´ıtulo haremos referencia a estos dos programas. No los pierdas
2 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
de vista.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 1 Compara los programas sumatorio.py y sumatorio.c. Analiza sus semejanzas y diferen-
cias. ¿Qu´e funci´on desempe˜nan las llaves en sumatorio.c? ¿Qu´e funci´on crees que desempe˜nan
las l´ıneas 6 y 7 del programa C? ¿A qu´e elemento de Python se parecen las dos primeras l´ıneas
de sumatorio.c? ¿Qu´e similitudes y diferencias aprecias entre las estructuras de control de
Python y C? ¿C´omo crees que se interpreta el bucle for del programa C? ¿Por qu´e algunas
l´ıneas de sumatorio.c finalizan en punto y coma y otras no? ¿Qu´e diferencias ves entre los
comentarios Python y los comentarios C?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Un poco de historia
C ya tiene sus a˜nitos. El nacimiento de C est´a estrechamente vinculado al del sistema opera-
tivo Unix. El investigador Ken Thompson, de AT&T, la compa˜n´ıa telef´onica estadounidense,
se propuso dise˜nar un nuevo sistema operativo a principios de los setenta. Dispon´ıa de un
PDP-7 en el que codific´o una primera versi´on de Unix en lenguaje ensamblador. Pronto se
impuso la conveniencia de desarrollar el sistema en un lenguaje de programaci´on de alto
nivel, pero la escasa memoria del PDP-7 (8K de 18 bits) hizo que ideara el lenguaje de
programaci´on B, una versi´on reducida de un lenguaje ya existente: BCPL. El lenguaje C
apareci´o como un B mejorado, fruto de las demandas impuestas por el desarrollo de Unix.
Dennis Ritchie fue el encargado del dise˜no del lenguaje C y de la implementaci´on de un
compilador para ´el sobre un PDP-11.
C ha sufrido numerosos cambios a lo largo de su historia. La primera versi´on ((estable))
del lenguaje data de 1978 y se conoce como ((K&R C)), es decir, ((C de Kernighan y Ritchie)).
Esta versi´on fue descrita por sus autores en la primera edici´on del libro ((The C Programming
Language)) (un aut´entico ((best-seller)) de la inform´atica). La adopci´on de Unix como siste-
ma operativo de referencia en las universidades en los a˜nos 80 populariz´o enormemente el
lenguaje de programaci´on C. No obstante, C era atractivo por s´ı mismo y parec´ıa satisfacer
una demanda real de los programadores: disponer de un lenguaje de alto nivel con ciertas
caracter´ısticas propias de los lenguajes de bajo nivel (de ah´ı que a veces se diga que C es
un lenguaje de nivel intermedio).
La experiencia con lenguajes de programaci´on dise˜nados con anterioridad, como Lisp o
Pascal, demuestra que cuando el uso de un lenguaje se extiende es muy probable que proli-
feren variedades dialectales y extensiones para aplicaciones muy concretas, lo que dificulta
enormemente el intercambio de programas entre diferentes grupos de programadores. Para
evitar este problema se suele recurrir a la creaci´on de un comit´e de expertos que define la
versi´on oficial del lenguaje. El comit´e ANSI X3J9 (ANSI son las siglas del American National
Standards Institute), creado en 1983, considera la inclusi´on de aquellas extensiones y mejo-
ras que juzga de suficiente inter´es para la comunidad de programadores. El 14 de diciembre
de 1989 se acord´o qu´e era el ((C est´andar)) y se public´o el documento con la especificaci´on
en la primavera de 1990. El est´andar se divulg´o con la segunda edici´on de ((The C Pro-
gramming Language)), de Brian Kernighan y Dennis Ritchie. Un comit´e de la International
Standards Office (ISO) ratific´o el documento del comit´e ANSI en 1992, convirti´endolo as´ı
en un est´andar internacional. Durante mucho tiempo se conoci´o a esta versi´on del lenguaje
como ANSI-C para distinguirla as´ı del K&R C. Ahora se prefiere denominar a esta variante
C89 (o C90) para distinguirla de la revisi´on que se public´o en 1999, la que se conoce por
C99 y que es la versi´on est´andar de C que estudiaremos.
C ha tenido un gran impacto en el dise˜no de otros muchos lenguajes. Ha sido, por
ejemplo, la base para definir la sintaxis y ciertos aspectos de la sem´antica de lenguajes tan
populares como Java y C++.
1.1. C es un lenguaje compilado
Python y C no s´olo se diferencian en su sintaxis, tambi´en son distintos en el modo en que se
traducen los programas a c´odigo de m´aquina y en el modo en que ejecutamos los programas.
Python es un lenguaje interpretado: para ejecutar un programa Python, suministramos al
int´erprete un fichero de texto (t´ıpicamente con extensi´on ((.py))) con su c´odigo fuente. Si
deseamos ejecutar sumatorio.py, por ejemplo, hemos de escribir python sumatorio.py
Introducci´on a la Programaci´on con C 3
1.1 C es un lenguaje compilado
en la l´ınea de ´ordenes Unix. Como resultado, el int´erprete va leyendo y ejecutando paso a
paso el programa. Para volver a ejecutarlo, has de volver a escribir python sumatorio.py
en la l´ınea de ´ordenes, con lo que se repite el proceso completo de traducci´on y ejecuci´on
paso a paso. Aunque no modifiquemos el c´odigo fuente, es necesario interpretarlo (traducir
y ejecutar paso a paso) nuevamente.
sumatorio.py Int´erprete Python Resultados
C es un lenguaje compilado: antes de ejecutar un programa escrito por nosotros, sumi-
nistramos su c´odigo fuente (en un fichero con extensi´on ((.c))) a un compilador de C.
El compilador lee y analiza todo el programa. Si el programa est´a correctamente escrito
seg´un la definici´on del lenguaje, el compilador genera un nuevo fichero con su traducci´on
a c´odigo de m´aquina, y si no, muestra los errores que ha detectado. Para ejecutar el pro-
grama utilizamos el nombre del fichero generado. Si no modificamos el c´odigo fuente, no
hace falta que lo compilemos nuevamente para volver a ejecutar el programa: basta con
volver a ejecutar el fichero generado por el compilador.
Para ejecutar sumatorio.c, por ejemplo, primero hemos de usar un compilador para
producir un nuevo fichero llamado sumatorio.
sumatorio.c Compilador de C sumatorio
Podemos ejecutar el programa escribiendo sumatorio en la l´ınea de ´ordenes Unix.1
sumatorio Resultados
Si queremos volver a ejecutarlo, basta con escribir de nuevo sumatorio; no es necesario
volver a compilar el contenido del fichero sumatorio.c.
sumatorio Resultados
La principal ventaja de compilar los programas es que se gana en velocidad de ejecuci´on,
ya que cuando el programa se ejecuta est´a completamente traducido a c´odigo de m´aquina y
se ahorra el proceso de ((traducci´on simult´anea)) que conlleva interpretar un programa. Pero,
adem´as, como se traduce a c´odigo de m´aquina en una fase independiente de la fase de ejecuci´on,
el programa traductor puede dedicar m´as tiempo a intentar encontrar la mejor traducci´on
posible, la que proporcione el programa de c´odigo de m´aquina m´as r´apido (o que consuma
menos memoria).
Nosotros usaremos un compilador concreto de C: gcc (en su versi´on 3.2 o superior)2
. Su
forma de uso m´as b´asica es ´esta:
gcc fichero.c -o fichero ejecutable
La opci´on -o es abreviatura de ((output)), es decir, ((salida)), y a ella le sigue el nombre del
fichero que contendr´a la traducci´on a c´odigo m´aquina del programa. Debes tener presente que
dicho fichero s´olo se genera si el programa C est´a correctamente escrito.
Si queremos compilar el programa sumatorio.c hemos de usar una opci´on especial:
gcc sumatorio.c -lm -o sumatorio
La opci´on -lm se debe usar siempre que nuestro programa utilice funciones del m´odulo
matem´atico (como sqrt, que se usa en sumatorio.c). Ya te indicaremos por qu´e en la secci´on
dedicada a presentar el m´odulo matem´atico de C.
1Por razones de seguridad es probable que no baste con escribir sumatorio para poder ejecutar un programa
con ese nombre y que reside en el directorio activo. Si es as´ı, prueba con ./sumatorio.
2La versi´on 3.2 de gcc es la primera en ofrecer un soporte suficiente de C99. Si usas una versi´on anterior, es
posible que algunos (pocos) programas del libro no se compilen correctamente.
4 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
C99 y gcc
Por defecto, gcc acepta programas escritos en C89 con extensiones introducidas por GNU
(el grupo de desarrolladores de muchas herramientas de Linux). Muchas de esas extensiones
de GNU forman ya parte de C99, as´ı que gcc es, por defecto, el compilador de un lenguaje
intermedio entre C89 y C99. Si en alg´un momento da un aviso indicando que no puede
compilar alg´un programa porque usa caracter´ısticas propias del C99 no disponibles por
defecto, puedes forzarle a compilar en ((modo C99)) as´ı:
gcc programa.c -std=c99 -o programa
Has de saber, no obstante, que gcc a´un no soporta el 100% de C99 (aunque s´ı todo lo
que te explicamos en este texto).
El compilador gcc acepta muchas otras variantes de C. Puedes forzarle a aceptar una
en particular ((asignando)) a la opci´on -std el valor c89, c99, gnu89 o gnu99.
1.2. Traduciendo de Python a C: una gu´ıa r´apida
Empezaremos por presentar de forma concisa c´omo traducir la mayor parte de los programas
Python que aprendimos a escribir en los cap´ıtulos 3 y 4 del primer volumen a programas equi-
valentes en C. En secciones posteriores entraremos en detalle y nos dedicaremos a estudiar las
muchas posibilidades que ofrece C a la hora de seleccionar tipos de datos, presentar informaci´on
con sentencias de impresi´on en pantalla, etc.
1. Los programas (sencillos) presentan, generalmente, este aspecto:
1 #include <stdio.h>
2
3 Posiblemente otros ((#include))
4
5 int main(void)
6 {
7 Programa principal.
8
9 return 0;
10 }
Hay, pues, dos zonas: una inicial cuyas l´ıneas empiezan por #include (equivalentes a las
sentencias import de Python) y una segunda que empieza con una l´ınea ((int main(void)))
y comprende las sentencias del programa principal mas una l´ınea ((return 0;)), encerradas
todas ellas entre llaves ({ y }).
De ahora en adelante, todo texto comprendido entre llaves recibir´a el nombre de bloque.
2. Toda variable debe declararse antes de ser usada. La declaraci´on de la variable consiste
en escribir el nombre de su tipo (int para enteros y float para flotantes)3
seguida del
identificador de la variable y un punto y coma. Por ejemplo, si vamos a usar una variable
entera con identificador a y una variable flotante con identificador b, nuestro programa
las declarar´a as´ı:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a;
6 float b;
7
8 Sentencias donde se usan las variables.
9
10 return 0;
11 }
3Recuerda que no estudiaremos las variables de tipo cadena hasta el pr´oximo cap´ıtulo.
Introducci´on a la Programaci´on con C 5
1.2 Traduciendo de Python a C: una gu´ıa r´apida
No es obligatorio que la declaraci´on de las variables tenga lugar justo al principio del
bloque que hay debajo de la l´ınea ((int main(void))), pero s´ı conveniente.4
Si tenemos que declarar dos o m´as variables del mismo tipo, podemos hacerlo en una
misma l´ınea separando los identificadores con comas. Por ejemplo, si las variables x, y y
z son todas de tipo float, podemos recurrir a esta forma compacta de declaraci´on:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 float x, y, z;
6
7 ...
8
9 return 0;
10 }
3. Las sentencias de asignaci´on C son similares a las sentencias de asignaci´on Python: a
mano izquierda del s´ımbolo igual (=) se indica la variable a la que se va a asignar el valor
que resulta de evaluar la expresi´on que hay a mano derecha. Cada sentencia de asignaci´on
debe finalizar con punto y coma.
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a;
6 float b;
7
8 a = 2;
9 b = 0.2;
10
11 return 0;
12 }
Como puedes ver, los n´umeros enteros y flotantes se representan igual que en Python.
4. Las expresiones se forman con los mismos operadores que aprendimos en Python. Bueno,
hay un par de diferencias:
Los operadores Python and, or y not se escriben en C, respectivamente, con &&, ||
y !;
No hay operador de exponenciaci´on (que en Python era **).
Hay operadores para la conversi´on de tipos. Si en Python escrib´ıamos float(x) para
convertir el valor de x a flotante, en C escribiremos (float) x para expresar lo mismo.
F´ıjate en c´omo se disponen los par´entesis: los operadores de conversi´on de tipos son
de la forma (tipo).
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a;
6 float b;
7
8 a = 13 % 2;
9 b = 2.0 / (1.0 + 2 - (a + 1));
10
11 return 0;
12 }
4En versiones de C anteriores a C99 s´ı era obligatorio que las declaraciones se hicieran al principio de un
bloque. C99 permite declarar una variable en cualquier punto del programa, siempre que ´este sea anterior al
primer uso de la misma.
6 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
Las reglas de asociatividad y precedencia de los operadores son casi las mismas que apren-
dimos en Python. Hay m´as operadores en C y los estudiaremos m´as adelante.
5. Para mostrar resultados por pantalla se usa la funci´on printf . La funci´on recibe uno o
m´as argumentos separados por comas:
primero, una cadena con formato, es decir, con marcas de la forma %d para re-
presentar enteros y marcas %f para representar flotantes (en los que podemos usar
modificadores para, por ejemplo, controlar la cantidad de espacios que ocupar´a el
valor o la cantidad de cifras decimales de un n´umero flotante);
y, a continuaci´on, las expresiones cuyos valores se desea mostrar (debe haber una
expresi´on por cada marca de formato).
escribe.c escribe.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a;
6 float b;
7
8 a = 13 % 2;
9 b = 2.0 / (1.0 + 2 - (a + 1));
10
11 printf ("El valor de a es %d y el de b es %fn", a, b);
12
13 return 0;
14 }
La cadena con formato debe ir encerrada entre comillas dobles, no simples. El car´acter
de retorno de carro (n) es obligatorio si se desea finalizar la impresi´on con un salto de
l´ınea. (Observa que, a diferencia de Python, no hay operador de formato entre la cadena
de formato y las expresiones: la cadena de formato se separa de la primera expresi´on con
una simple coma).
Como puedes ver, todas las sentencias de los programas C que estamos presentando fina-
lizan con punto y coma.
6. Para leer datos de teclado has de usar la funci´on scanf . F´ıjate en este ejemplo:
lee y escribe.c lee y escribe.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a;
6 float b;
7
8 scanf ("%d", &a);
9 scanf ("%f", &b);
10
11 printf ("El valor de a es %d y el de b es %fn", a, b);
12
13 return 0;
14 }
La l´ınea 8 lee de teclado el valor de un entero y lo almacena en a. La l´ınea 9 lee de
teclado el valor de un flotante y lo almacena en b. Observa el uso de marcas de formato
en el primer argumento de scanf : %d se˜nala la lectura de un int y %f la de un float. El
s´ımbolo & que precede al identificador de la variable en la que se almacena el valor le´ıdo
es obligatorio para variables de tipo escalar.
Si deseas mostrar por pantalla un texto que proporcione informaci´on acerca de lo que el
usuario debe introducir, hemos de usar nuevas sentencias printf :
Introducci´on a la Programaci´on con C 7
1.2 Traduciendo de Python a C: una gu´ıa r´apida
lee mejor y escribe.c lee mejor y escribe.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a;
6 float b;
7
8 printf ("Introduce un entero a: ");
9 scanf ("%d", &a);
10 printf ("Y ahora un flotante b: ");
11 scanf ("%f", &b);
12
13 printf ("El valor de a es %d y el de b es %fn", a, b);
14
15 return 0;
16 }
7. La sentencia if de Python presenta un aspecto similar en C:
si es par.c si es par.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a;
6
7 printf ("Introduce un entero a: ");
8 scanf ("%d", &a);
9
10 if (a % 2 == 0) {
11 printf ("El valor de a es par.n");
12 printf ("Es curioso.n");
13 }
14
15 return 0;
16 }
Ten en cuenta que:
la condici´on va encerrada obligatoriamente entre par´entesis;
y el bloque de sentencias cuya ejecuci´on est´a supeditada a la satisfacci´on de la con-
dici´on va encerrado entre llaves (aunque matizaremos esta afirmaci´on m´as adelante).
Naturalmente, puedes anidar sentencias if.
si es par y positivo.c si es par y positivo.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a;
6
7 printf ("Introduce un entero a: ");
8 scanf ("%d", &a);
9
10 if (a % 2 == 0) {
11 printf ("El valor de a es par.n");
12 if (a > 0) {
13 printf ("Y, adem´as, es positivo.n");
14 }
15 }
16
8 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
17 return 0;
18 }
Tambi´en hay sentencia if-else en C:
par o impar.c par o impar.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a;
6
7 printf ("Introduce un entero a: ");
8 scanf ("%d", &a);
9
10 if (a % 2 == 0) {
11 printf ("El valor de a es par.n");
12 }
13 else {
14 printf ("El valor de a es impar.n");
15 }
16
17 return 0;
18 }
No hay, sin embargo, sentencia if-elif, aunque es f´acil obtener el mismo efecto con una
sucesi´on de if-else if:
tres casos.c tres casos.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a;
6
7 printf ("Introduce un entero a: ");
8 scanf ("%d", &a);
9
10 if (a > 0) {
11 printf ("El valor de a es positivo.n");
12 }
13 else if (a == 0) {
14 printf ("El valor de a es nulo.n");
15 }
16 else if (a < 0) {
17 printf ("El valor de a es negativo.n");
18 }
19 else {
20 printf ("Es imposible mostrar este mensaje.n");
21 }
22
23 return 0;
24 }
8. La sentencia while de C es similar a la de Python, pero has de tener en cuenta la
obligatoriedad de los par´entesis alrededor de la condici´on y que las sentencias que se
pueden repetir van encerradas entre un par de llaves:
cuenta atras.c cuenta atras.c
1 #include <stdio.h>
2
3 int main(void)
4 {
Introducci´on a la Programaci´on con C 9
1.2 Traduciendo de Python a C: una gu´ıa r´apida
5 int a;
6
7 printf ("Introduce un entero a: ");
8 scanf ("%d", &a);
9
10 while (a > 0) {
11 printf ("%d", a);
12 a -= 1;
13 }
14 printf ("
!
Boom!n");
15
16 return 0;
17 }
9. Tambi´en puedes usar la sentencia break en C:
primo.c primo.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b;
6
7 printf ("Introduce un entero a: ");
8 scanf ("%d", &a);
9
10 b = 2;
11 while (b < a) {
12 if (a % b == 0) {
13 break;
14 }
15 b += 1;
16 }
17 if (b == a) {
18 printf ("%d es primo.n", a);
19 }
20 else {
21 printf ("%d no es primo.n", a);
22 }
23
24 return 0;
25 }
10. Los m´odulos C reciben el nombre de bibliotecas y se importan con la sentencia #include.
Ya hemos usado #include en la primera l´ınea de todos nuestros programas: #include
<stdio.h>. Gracias a ella hemos importado las funciones de entrada/salida scanf y printf .
No se puede importar una sola funci´on de una biblioteca: debes importar el contenido
completo de la biblioteca.
Las funciones matem´aticas pueden importarse del m´odulo matem´atico con #include
<math.h> y sus nombres son los mismos que vimos en Python (sin para el seno, cos
para el coseno, etc.).
raiz cuadrada.c raiz cuadrada.c
1 #include <stdio.h>
2 #include <math.h>
3
4 int main(void)
5 {
6 float b;
7
8 printf ("Escribe un flotante: ");
9 scanf ("%f", &b);
10 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
10
11 if (b >= 0.0) {
12 printf ("Su ra´ız cuadrada es %f.n", sqrt(b));
13 }
14 else {
15 printf ("No puedo calcular su ra´ız cuadrada.n");
16 }
17
18 return 0;
19 }
No hay funciones predefinidas en C. Muchas de las que estaban predefinidas en Python
pueden usarse en C, pero import´andolas de bibliotecas. Por ejemplo, abs (valor absolu-
to) puede importarse del m´odulo stdlib.h (por ((standard library)), es decir, ((biblioteca
est´andar))).
Las (aproximaciones a las) constantes π y e se pueden importar de la biblioteca ma-
tem´atica, pero sus identificadores son ahora M_PI y M_E, respectivamente.
No est´a mal: ya sabes traducir programas Python sencillos a C (aunque no sabemos traducir
programas con definiciones de funci´on, ni con variables de tipo cadena, ni con listas, ni con
registros, ni con acceso a ficheros. . . ). ¿Qu´e tal practicar con unos pocos ejercicios?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 2 Traduce a C este programa Python.
1 a = int(raw_input(’Dame el primer n´umero: ’))
2 b = int(raw_input(’Dame el segundo n´umero: ’))
3
4 if a >= b:
5 maximo = a
6 else:
7 maximo = b
8
9 print ’El m´aximo es’, maximo
· 3 Traduce a C este programa Python.
1 n = int(raw_input(’Dame un n´umero: ’))
2 m = int(raw_input(’Dame otro n´umero: ’))
3
4 if n * m == 100:
5 print ’El producto %d * %d es igual a 100’ % (n, m)
6 else:
7 print ’El producto %d * %d es distinto de 100’ % (n, m)
· 4 Traduce a C este programa Python.
1 from math import sqrt
2
3 x1 = float(raw_input("Punto 1, coordenada x: "))
4 y1 = float(raw_input("Punto 1, coordenada y: "))
5 x2 = float(raw_input("Punto 2, coordenada x: "))
6 y2 = float(raw_input("Punto 2, coordenada y: "))
7 dx = x2 - x1
8 dy = y2 - y1
9 distancia = sqrt(dx**2 + dy**2)
10 print ’La distancia entre los puntos es: ’, distancia
· 5 Traduce a C este programa Python.
1 a = float(raw_input(’Valor de a: ’))
2 b = float(raw_input(’Valor de b: ’))
3
4 if a != 0:
5 x = -b/a
Introducci´on a la Programaci´on con C 11
1.3 Estructura t´ıpica de un programa C
6 print ’Soluci´on: ’, x
7 else:
8 if b != 0:
9 print ’La ecuaci´on no tiene soluci´on.’
10 else:
11 print ’La ecuaci´on tiene infinitas soluciones.’
· 6 Traduce a C este programa Python.
1 from math import log
2
3 x = 1.0
4 while x < 10.0:
5 print x, ’t’, log(x)
6 x = x + 1.0
· 7 Traduce a C este programa Python.
1 n = 1
2 while n < 6:
3 i = 1
4 while i < 6:
5 print n*i, ’t’,
6 i = i + 1
7 print
8 n = n + 1
· 8 Traduce a C este programa Python.
1 from math import pi
2
3 opcion = 0
4 while opcion != 4:
5 print ’Escoge una opci´on: ’
6 print ’1) Calcular el di´ametro.’
7 print ’2) Calcular el per´ımetro.’
8 print ’3) Calcular el ´area.’
9 print ’4) Salir.’
10 opcion = int(raw_input(’Teclea 1, 2, 3 o 4 y pulsa el retorno de carro: ’))
11
12 radio = float(raw_input(’Dame el radio de un c´ırculo: ’))
13
14 if opcion == 1:
15 diametro = 2 * radio
16 print ’El di´ametro es’, diametro
17 elif opcion == 2:
18 perimetro = 2 * pi * radio
19 print ’El per´ımetro es’, perimetro
20 elif opcion == 3:
21 area = pi * radio ** 2
22 print ’El ´area es’, area
23 elif opcion < 0 or opcion > 4:
24 print ’S´olo hay cuatro opciones: 1, 2, 3 o 4. T´u has tecleado’, opcion
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Ya es hora, pues, de empezar con los detalles de C.
1.3. Estructura t´ıpica de un programa C
Un programa C no es m´as que una colecci´on de declaraciones de variables globales y de defini-
ciones de constantes, macros, tipos y funciones. Una de las funciones es especial: se llama main
(que en ingl´es significa ((principal))) y contiene el c´odigo del programa principal. No nos deten-
dremos a explicar la sintaxis de la definici´on de funciones hasta el cap´ıtulo 3, pero debes saber
ya que la definici´on de la funci´on main empieza con ((int main (void))) y sigue con el cuerpo
de la funci´on encerrado entre un par de llaves. La funci´on main debe devolver un valor entero
12 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
al final (t´ıpicamente el valor 0), por lo que finaliza con una sentencia return que devuelve el
valor 0.5
La estructura t´ıpica de un programa C es ´esta:
Importaci´on de funciones, variables, constantes, etc.
Definici´on de constantes y macros.
Definici´on de nuevos tipos de datos.
Declaraci´on de variables globales.
Definici´on de funciones.
int main(void)
{
Declaraci´on de variables propias del programa principal (o sea, locales a main).
Programa principal.
return 0;
}
Un fichero con extensi´on ((.c)) que no define la funci´on main no es un programa C completo.
Si, por ejemplo, tratamos de compilar este programa incorrecto (no define main):
E sin main.c E
1 int a;
2 a = 1;
el compilador muestra el siguiente mensaje (u otro similar, seg´un la versi´on del compilador que
utilices):
$ gcc sin_main.c -o sin_main
sin_main.c:2: warning: data definition has no type or storage class
/usr/lib/crt1.o: En la funci´on ‘_start’:
/usr/lib/crt1.o(.text+0x18): referencia a ‘main’ sin definir
collect2: ld returned 1 exit status
F´ıjate en la tercera l´ınea del mensaje de error: ((referencia a ‘main’ sin definir)).
1.4. C es un lenguaje de formato libre
As´ı como en Python la indentaci´on determina los diferentes bloques de un programa, en C la
indentaci´on es absolutamente superflua: indentamos los programas ´unicamente para hacerlos
m´as legibles. En C se sabe d´onde empieza y d´onde acaba un bloque porque ´este est´a encerrado
entre una llave abierta ({) y otra cerrada (}).
He aqu´ı un ejemplo de bloques anidados en el que hemos indentado el c´odigo para facilitar
su lectura:
minimo.c minimo.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b, c, minimo;
6
7 scanf ("%d", &a);
8 scanf ("%d", &b);
9 scanf ("%d", &c);
5El valor 0 se toma, por un convenio, como se˜nal de que el programa finaliz´o correctamente. El sistema
operativo Unix recibe el valor devuelto con el return y el int´erprete de ´ordenes, por ejemplo, puede tomar una
decisi´on acerca de qu´e hacer a continuaci´on en funci´on del valor devuelto.
Introducci´on a la Programaci´on con C 13
1.4 C es un lenguaje de formato libre
10 if (a < b) {
11 if (a < c) {
12 minimo = a;
13 }
14 else {
15 minimo = c;
16 }
17 }
18 else {
19 if (b < c) {
20 minimo = b;
21 }
22 else {
23 minimo = c;
24 }
25 }
26 printf ("%dn", minimo);
27 return 0;
28 }
Este programa podr´ıa haberse escrito como sigue y ser´ıa igualmente correcto:
minimo 1.c minimo.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b, c, minimo;
6
7 scanf ("%d", &a);
8 scanf ("%d", &b);
9 scanf ("%d", &c);
10 if (a < b) {
11 if (a < c) { minimo = a; }
12 else { minimo = c; }
13 }
14 else {
15 if (b < c) { minimo = b; }
16 else { minimo = c; }
17 }
18 printf ("%dn", minimo);
19 return 0;
20 }
Cuando un bloque consta de una sola sentencia no es necesario encerrarla entre llaves. Aqu´ı
tienes un ejemplo:
minimo 2.c minimo.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b, c, minimo;
6
7 scanf ("%d", &a);
8 scanf ("%d", &b);
9 scanf ("%d", &c);
10 if (a < b) {
11 if (a < c) minimo = a;
12 else minimo = c;
13 }
14 else {
15 if (b < c) minimo = b;
16 else minimo = c;
14 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
17 }
18 printf ("%dn", minimo);
19 return 0;
20 }
De hecho, como if-else es una ´unica sentencia, tambi´en podemos suprimir las llaves restantes:
minimo 3.c minimo.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b, c, minimo;
6
7 scanf ("%d", &a);
8 scanf ("%d", &b);
9 scanf ("%d", &c);
10 if (a < b)
11 if (a < c) minimo = a;
12 else minimo = c;
13 else
14 if (b < c) minimo = b;
15 else minimo = c;
16 printf ("%dn", minimo);
17 return 0;
18 }
Debes tener cuidado, no obstante, con las ambig¨uedades que parece producir un s´olo else y
dos if:
primero es minimo 1.c E primero es minimo.c E
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b, c, minimo;
6
7 scanf ("%d", &a);
8 scanf ("%d", &b);
9 scanf ("%d", &c);
10 if (a < b)
11 if (a < c)
12 printf ("El primero es el m´ınimo.n");
13 else
14 printf ("El primero no menor que el segundo.n");
15 printf ("%dn", minimo);
16 return 0;
17 }
¿Cu´al de los dos if se asocia al else? C usa una regla: el else se asocia al if m´as pr´oximo (en el
ejemplo, el segundo). Seg´un esa regla, el programa anterior no es correcto. El sangrado sugiere
una asociaci´on entre el primer if y el else que no es la que interpreta C. Para que C ((entienda))
la intenci´on del autor es necesario que explicites con llaves el alcance del primer if:
primero es minimo 2.c primero es minimo.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b, c, minimo;
6
7 scanf ("%d", &a);
8 scanf ("%d", &b);
9 scanf ("%d", &c);
10 if (a < b) {
Introducci´on a la Programaci´on con C 15
1.4 C es un lenguaje de formato libre
11 if (a < c)
12 printf ("El primero es el m´ınimo.n");
13 }
14 else
15 printf ("El primero no es menor que el segundo.n");
16 printf ("%dn", minimo);
17 return 0;
18 }
Ahora que has adquirido la pr´actica de indentar los programas gracias a la disciplina im-
puesta por Python, s´ıguela siempre, aunque programes en C y no sea necesario.
La indentaci´on no importa. . . pero nadie se pone de acuerdo
En C no es obligatorio indentar los programas, aunque todos los programadores est´an de
acuerdo en que un programa sin una ((correcta)) indentaci´on es ilegible. ¡Pero no hay consenso
en lo que significa indentar ((correctamente))! Hay varios estilos de indentaci´on en C y cada
grupo de desarrolladores escoge el que m´as le gusta. Te presentamos unos pocos estilos:
a) La llave abierta se pone en la misma l´ınea con la estructura de control y la llave de cierre
va en una l´ınea a la altura del inicio de la estructura:
if (a==1) {
b = 1;
c = 2;
}
b) ´Idem, pero la llave de cierre se dispone un poco a la derecha:
if (a==1) {
b = 1;
c = 2;
}
c) La llave abierta va en una l´ınea sola, al igual que la llave cerrada. Ambas se disponen a
la altura de la estructura que gobierna el bloque:
if (a==1)
{
b = 1;
c = 2;
}
d) ´Idem, pero las dos llaves se disponen m´as a la derecha y el contenido del bloque m´as a
la derecha:
if (a==1)
{
b = 1;
c = 2;
}
e) Y a´un otro, con las llaves a la misma altura que el contenido del bloque:
if (a==1)
{
b = 1;
c = 2;
}
No hay un estilo mejor que otro. Es cuesti´on de puro convenio. A´un as´ı, hay m´as de
una discusi´on subida de tono en los grupos de debate para desarrolladores de C. Incre´ıble,
¿no? En este texto hemos optado por el primer estilo de la lista (que, naturalmente, es el
((correcto)) ;-)) para todas las construcciones del lenguaje a excepci´on de la definici´on de
funciones (como main), que sigue el convenio de indentaci´on que relacionamos en tercer
lugar.
Una norma: las sentencias C acaban con un punto y coma. Y una excepci´on a la norma: no
16 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
hace falta poner punto y coma tras una llave cerrada.6
Dado que las sentencias finalizan con punto y coma, no tienen por qu´e ocupar una l´ınea.
Una sentencia como ((a = 1;)) podr´ıa escribirse, por ejemplo, en cuatro l´ıneas:
a
=
1
;
Pero aunque sea l´ıcito escribir as´ı esa sentencia, no tienen ning´un sentido y hace m´as dif´ıcil la
comprensi´on del programa. Recuerda: vela siempre por la legibilidad de los programas.
Tambi´en podemos poner m´as de una sentencia en una misma l´ınea, pues el compilador sabr´a
d´onde empieza y acaba cada una gracias a los puntos y comas, las llaves, etc. El programa
sumatorio.c, por ejemplo, podr´ıa haberse escrito as´ı:
sumatorio ilegible.c sumatorio ilegible.c
1 #include <stdio.h>
2 #include <math.h>
3 int main(void) { int a, b, i; float s; /* Pedir l´ımites inferior y superior. */ printf (
4 "L´ımite inferior:"); scanf ("%d", &a); while (a < 0) { printf ("No puede ser negativon");
5 printf ("L´ımite inferior:"); scanf ("%d", &a); } printf ("L´ımite superior:"); scanf ("%d",
6 &b); while (b < a) { printf ("No puede ser mayor que %dn", a); printf ("L´ımite superior:")
7 ; scanf ("%d", &b); } /* Calcular el sumatorio de la ra´ız cuadrada de i para i entre a y b. */ s =
8 0.0; for (i = a; i <= b; i++) { s += sqrt(i); } /* Mostrar el resultado. */ printf (
9 "Sumatorio de ra´ıces "); printf ("de %d a %d: %fn", a, b, s); return 0;}
Obviamente, hubiera sido una mala elecci´on: un programa escrito as´ı, aunque correcto, es com-
pletamente ilegible.7
Un programador de C experimentado hubiera escrito sumatorio.c utilizando llaves s´olo don-
de resultan necesarias y, probablemente, utilizando unas pocas l´ıneas menos. Estudia las dife-
rencias entre la primera versi´on de sumatorio.c y esta otra:
sumatorio 1.c sumatorio.c
1 #include <stdio.h>
2 #include <math.h>
3
4 int main(void)
5 {
6 int a, b, i;
7 float s;
8
9 /* Pedir l´ımites inferior y superior. */
10 printf ("L´ımite inferior:"); scanf ("%d", &a);
11 while (a < 0) {
12 printf ("No puede ser negativonL´ımite inferior:"); scanf ("%d", &a);
13 }
14
15 printf ("L´ımite superior:"); scanf ("%d", &b);
16 while (b < a) {
17 printf ("No puede ser mayor que %dn L´ımite superior:", a); scanf ("%d", &b);
18 }
19
20 /* Calcular el sumatorio de la ra´ız cuadrada de i para i entre a y b. */
21 s = 0.0;
22 for (i = a; i <= b; i++) s += sqrt(i);
23
24 /* Mostrar el resultado. */
25 printf ("Sumatorio de ra´ıces de %d a %d: %fn", a, b, s);
26
27 return 0;
28 }
6Habr´a una excepci´on a esta norma: las construcciones struct, cuya llave de cierre debe ir seguida de un
punto y coma.
7Quiz´a hayas reparado en que las l´ıneas que empiezan con #include son especiales y que las tratamos de
forma diferente: no se puede jugar con su formato del mismo modo que con las dem´as: cada sentencia #include
debe ocupar una l´ınea y el car´acter # debe ser el primero de la l´ınea.
Introducci´on a la Programaci´on con C 17
1.4 C es un lenguaje de formato libre
International Obfuscated C Code Contest
Es posible escribir programas ilegibles en C, ¡hasta tal punto que hay un concurso inter-
nacional de programas ilegibles escritos en C!: el International Obfuscated C Code Contest
(IOCCC). Aqu´ı tienes un programa C (en K&R C, ligeramente modificado para que pueda
compilarse con gcc) que concurs´o en 1989:
extern int
errno
;char
grrr
;main( r,
argv, argc ) int argc ,
r ; char *argv[];{int P( );
#define x int i=0, j=0,cc[4];printf(" choo choon" ) ;
x ;if (P( ! i ) | cc[ ! j ]
& P(j )>2 ? j : i ){* argv[i++ +!-i]
; for (i= 0;; i++ );
_exit(argv[argc- 2 / cc[1*argc]|-1<<4 ] ) ;printf("%d",P(""));}}
P ( a ) char a ; { a ; while( a > " B "
/* - by E ricM arsh all- */); }
¿Sabes qu´e hace? ¡S´olo imprime en pantalla ((choo choo))!
El siguiente programa es un generador de anagramas escrito por Andreas Gustafsson
(AG ;-)) y se present´o a la edici´on de 1992:
#include <stdio.h>
long a
[4],b[
4],c[4]
,d[0400],e=1;
typedef struct f{long g
,h,i[4] ,j;struct f*k;}f;f g,*
l[4096 ]; char h[256],*m,k=3;
long n (o, p,q)long*o,*p,*q;{
long r =4,s,i=0;for(;r--;s=i^
*o^*p, i=i&*p|(i|*p)&~*o++,*q
++=s,p ++);return i;}t(i,p)long*p
;{*c=d [i],n(a,c,b),n(p,b,p);}u(j)f*j;{j->h
=(j->g =j->i[0]|j->i[1]|j->i[2]|j->i[3])&4095;}v(
j,s)f* j; {int i; for(j->k->k&&v(j->k, ’ ’),fseek(
stdin, j->j, 0);i=getchar(),putchar(i-’n’?i:s),i-
’n’;);}w(o,r,j,x,p)f*o,*j;long p;{f q;int
s,i=o->h;q.k=o;r>i?j=l[r=i]:r<i&&
(s=r&~i)?(s|=s>>1, s|=s
>>2,s|=s>>4,s
|=s>>8
,j=l[r
=((r&i |s)&~(s>>1))-1&i]):0;--x;for
(;x&&!(p&i);p>>=1);for(;!x&&j;n(o->i,j->i,q.
i),u(&q),q.g||(q.j=j->j,v(&q,’n’)),j=j->k);for(;x;j=x
?j->k:0){for(;!j&&((r=(r&i)-1&i)-i&&(r&p)?2:(x=0));j=l[r]);!
x||(j->g&~o->g)||n (o->i,j->i,q.i)||(
u(&q), q.j=j ->j,q.g?w(&q
,r,j->k,x ,p):v(&q,
’n’)); }}y(){f
j;char *z,*p;
for(;m ? j.j=
ftell( stdin)
,7,(m= gets(m ))||w(
&g,315 *13,l[ 4095]
,k,64* 64)&0: 0;n(g
.i,j.i, b)||(u (&j),j.
k=l[j.h],l[j.h]= &j,y())){for(z= p=h;*z&&(
d[*z++]||(p=0)););for(z=p?n(j.i ,j.i,j.i)+h:"";
*z;t(*z++,j.i));}}main(o,p)char** p; {for(;m = *++p;)for(;*m-
’-’?*m:(k= -atoi(m))&0;d[*m]||(d[*m ]=e,e<<=1),t(*m++,g.i)); u(&
g),m=h
,y();}
El programa lee un diccionario de la entrada est´andar y recibe como argumentos el n´umero
de palabras del anagrama (precedido por un gui´on) y el texto del que se desea obtener
un anagrama. Si compilas el programa y lo ejecutas con un diccionario ingl´es y el texto
((Universitat Jaume I)) descubrir´as algunos anagramas curiosos. Para ver qu´e hace exacta-
mente, ejecuta
$ anagrama </usr/dict/words -3 universitat jaume i)
en el int´erprete de ´ordenes: por pantalla aparecer´an decenas de anagramas, entre ellos
((autism injure vitae)) y ((mutate via injuries)). Usando un diccionario espa˜nol y di-
ferentes n´umeros de palabras obtendr´as, entre otros, ´estos: ((mutis, vieja uterina)) o
((mi jeta nueva, tu iris)).
Ya sabes: puedes escribir programas ilegibles en C. ¡Procura que tus programas no
merezcan una menci´on de honor en el concurso!
Los lenguajes de programaci´on en los que el c´odigo no debe seguir un formato determinado
de l´ıneas y/o bloques se denominan de formato libre. Python no es un lenguaje de formato libre;
C s´ı.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 9 Este programa C incorrecto tiene varios errores que ya puedes detectar. Indica cu´ales son:
18 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
1 #include <stdio.h>
2
3 int a, b;
4
5 scanf ("%d", &a); scanf ("%d", &b)
6 while (a <= b):
7 scanf ("%d", &a)
8 scanf ("%d", &b)
9 printf ("%d %dn", a, b);
· 10 Indenta ((correctamente)) este programa C.
1 #include <stdio.h>
2 int main(void)
3 {
4 int a, b;
5 scanf ("%d", &a);
6 scanf ("%d", &b);
7 while(a > b) {
8 scanf ("%d", &a);
9 scanf ("%d", &b);
10 }
11 printf ("%d %dn", a, b);
12 return 0;
13 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.5. Hay dos tipos de comentario
C99 permite escribir comentarios de dos formas distintas. Una es similar a la de Python: se
marca el inicio de comentario con un s´ımbolo especial y ´este se prolonga hasta el final de l´ınea.
La marca especial no es #, sino //. El segundo tipo de comentario puede ocupar m´as de una
l´ınea: empieza con los caracteres /* y finaliza con la primera aparici´on del par de caracteres */.
En este ejemplo aparecen comentarios que abarcan m´as de una l´ınea:
maximo.c maximo.c
1 /*********************************************************************
2 * Un programa de ejemplo.
3 *--------------------------------------------------------------------
4 * Prop´osito: mostrar algunos efectos que se pueden lograr con
5 * comentarios de C
6 *********************************************************************/
8 #include <stdio.h>
9
10 /*---------------------------------------------------------------------
11 * Programa principal
12 *-------------------------------------------------------------------*/
14
15 int main(void)
16 {
17 int a, b, c; // Los tres n´umeros.
18 int m; // Variable para el m´aximo de los tres.
19
20 /* Lectura de un n´umero */
21 printf ("a: "); scanf ("%d", &a);
22 /* ... de otro ... */
23 printf ("b: "); scanf ("%d", &b);
24 /* ... y de otro m´as. */
25 printf ("c: "); scanf ("%d", &c);
26 if (a > b)
27 if (a > c) //En este caso a > b y a > c.
28 m = a;
Introducci´on a la Programaci´on con C 19
1.5 Hay dos tipos de comentario
29 else //Y en este otro caso b < a ≤ c.
30 m = c;
31 else
32 if (b > c) //En este caso a ≤ b y b > c.
33 m = b;
34 else //Y en este otro caso a ≤ b ≤ c.
35 m = c;
36 /* Impresi´on del resultado. */))
37 printf ("El m´aximo de %d, %d y %d es %dn", a, b, c, m);
38 return 0;
39 }
Uno de los comentarios empieza al principio de la l´ınea 1 y finaliza al final de la l´ıne 6 (sus
dos ´ultimos caracteres visibles son un asterisco y una barra). Hay otro que empieza en la l´ınea
10 y finaliza en al l´ınea 12. Y hay otros que usan las marcas /* y */ en l´ıneas como la 20 o la
22, aunque hubi´esemos podidos usar en ambos casos la marca //.
Los comentarios encerrados entre /* y */ no se pueden anidar. Este fragmento de programa
es incorrecto:
/* un /* comentario */ mal hecho */
¿Por qu´e? Parece que hay un comentario dentro de otro, pero no es as´ı: el comentario que
empieza en el primer par de caracteres /* acaba en el primer par de caracteres */, no en el
segundo. El texto del ´unico comentario aparece aqu´ı enmarcado:
/* un /* comentario */ mal hecho */
As´ı pues, el fragmento (( mal hecho */)) no forma parte de comentario alguno y no tiene sentido
en C, por lo que el compilador detecta un error.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 11 Haciendo pruebas durante el desarrollo de un programa hemos decidido comentar una
l´ınea del programa para que, de momento, no sea compilada. El programa nos queda as´ı:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b, i, j;
6
7 scanf ("%d", &a);
8 scanf ("%d", &b);
9 i = a;
10 j = 1;
11 while (i <= b) {
12 /* printf ("%d %dn", i, j); */
13 j *= 2;
14 i += 1;
15 }
16 printf ("%dn", j);
17 return 0;
18 }
Compilamos el programa y el compilador no detecta error alguno. Ahora decidimos comentar
el bucle while completo, as´ı que a˜nadimos un nuevo par de marcas de comentario (l´ıneas 11 y
17):
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b, i, j;
6
7 scanf ("%d", &a);
8 scanf ("%d", &b);
9 i = a;
20 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
10 j = 1;
11 /*
12 while (i <= b) {
13 /* printf ("%d %dn", i, j); */
14 j *= 2;
15 i += 1;
16 }
17 */
19 printf ("%dn", j);
20 return 0;
21 }
Al compilar nuevamente el programa aparecen mensajes de error. ¿Por qu´e?
· 12 ¿Da problemas este otro programa con comentarios?
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b, i, j;
6
7 scanf ("%d", &a);
8 scanf ("%d", &b);
9 i = a;
10 j = 1;
11 /*
12 while (i <= b) {
13 // printf ("%d %dn", i, j);
14 j *= 2;
15 i += 1;
16 }
17 */
19 printf ("%dn", j);
20 return 0;
21 }
· 13 ¿C´omo se interpreta esta sentencia?
1 i = x //*y*/z++
2 ;
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.6. Valores literales en C
Por valores literales nos referimos a valores de n´umeros, cadenas y caracteres dados expl´ı-
citamente. Afortunadamente, las reglas de escritura de literales en C son similares a las de
Python.
1.6.1. Enteros
Una forma natural de expresar un n´umero entero en C es mediante una secuencias de d´ıgitos.
Por ejemplo, 45, 0 o 124653 son enteros. Al igual que en Python, est´a prohibido insertar espacios
en blanco (o cualquier otro s´ımbolo) entre los d´ıgitos de un literal entero.
Hay m´as formas de expresar enteros. En ciertas aplicaciones resulta ´util expresar un n´umero
entero en base 8 (sistema octal) o en base 16 (sistema hexadecimal). Si una secuencia de d´ıgitos
empieza en 0, se entiende que codifica un n´umero en base 8. Por ejemplo, 010 es el entero 8 (en
base 10) y 0277 es el entero 191 (en base 10). Para codificar un n´umero en base 16 debes usar
el par de caracteres 0x seguido del n´umero en cuesti´on. El literal 0xff, por ejemplo, codifica el
valor decimal 255.
Pero a´un hay una forma m´as de codificar un entero, una que puede resultar extra˜na al
principio: mediante un car´acter entre comillas simples, que representa a su valor ASCII. El
Introducci´on a la Programaci´on con C 21
1.6 Valores literales en C
valor ASCII de la letra ((a min´uscula)), por ejemplo, es 97, as´ı que el literal ’a’ es el valor 97.
Hasta tal punto es as´ı que podemos escribir expresiones como ’a’+1, que es el valor 98 o, lo
que es lo mismo, ’b’.
Se puede utilizar cualquiera de las secuencias de escape que podemos usar con las cadenas.
El literal ’n’, por ejemplo, es el valor 10 (que es el c´odigo ASCII del salto de l´ınea).
Ni ord ni chr
En C no son necesarias las funciones ord o chr de Python, que convert´ıan caracteres en
enteros y enteros en caracteres. Como en C los caracteres son enteros, no resulta necesario
efectuar conversi´on alguna.
1.6.2. Flotantes
Los n´umeros en coma flotante siguen la misma sintaxis que los flotantes de Python. Un n´umero
flotante debe presentar parte decimal y/o exponente. Por ejemplo, 20.0 es un flotante porque
tiene parte decimal (aunque sea nula) y 2e1 tambi´en lo es, pero porque tiene exponente (es decir,
tiene una letra e seguida de un entero). Ambos representan al n´umero real 20.0. (Recuerda que
2e1 es 2 · 101
.) Es posible combinar en un n´umero parte decimal con exponente: 2.0e1 es un
n´umero en coma flotante v´alido.
1.6.3. Cadenas
As´ı como en Python puedes optar por encerrar una cadena entre comillas simples o dobles, en
C s´olo puedes encerrarla entre comillas dobles. Dentro de las cadenas puedes utilizar secuencias
de escape para representar caracteres especiales. Afortunadamente, las secuencias de escape son
las mismas que estudiamos en Python. Por ejemplo, el salto de l´ınea es n y la comilla doble
es "·.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 14 Traduce a cadenas C las siguientes cadenas Python:
1. "una cadena"
2. ’una cadena’
3. "una "cadena""
4. ’una "cadena"’
5. ’una ’cadena’’
6. "una cadena que ocupan dos l´ıneas"
7. "una cadena que no ocupa dos l´ıneas"
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Te relacionamos las secuencias de escape que puedes necesitar m´as frecuentemente:
22 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
Secuencia Valor
a (alerta): produce un aviso audible o visible.
b (backspace, espacio atr´as): el cursor retrocede un espacio a la izquierda.
f (form feed, alimentaci´on de p´agina): pasa a una nueva ((p´agina)).
n (newline, nueva l´ınea): el cursor pasa a la primera posici´on de la siguiente
l´ınea.
r (carriage return, retorno de carro): el cursor pasa a la primera posici´on
de la l´ınea actual.
t (tabulador): desplaza el cursor a la siguiente marca de tabulaci´on.
 muestra la barra invertida.
" muestra la comilla doble.
n´umero octal muestra el car´acter cuyo c´odigo ASCII (o IsoLatin) es el n´umero octal
indicado. El n´umero octal puede tener uno, dos o tres d´ıgitos octales.
Por ejemplo "60" equivale a "0", pues el valor ASCII del car´acter cero
es 48, que en octal es 60.
xn´umero hexadecimal ´ıdem, pero el n´umero est´a codificado en base 16 y puede tener uno o
dos d´ıgitos hexadecimales. Por ejemplo, "x30" tambi´en equivale a "0",
pues 48 en decimal es 30 en hexadecimal.
? muestra el interrogante.
Es pronto para aprender a utilizar variables de tipo cadena. Postergamos este asunto hasta
el apartado 2.2.
1.7. C tiene un rico juego de tipos escalares
En Python tenemos dos tipos num´ericos escalares: enteros y flotantes8
. En C hay una gran
variedad de tipos escalares en funci´on del n´umero de cifras o de la precisi´on con la que deseamos
trabajar, as´ı que nos permite tomar decisiones acerca del compromiso entre rango/precisi´on y
ocupaci´on de memoria: a menor rango/precisi´on, menor ocupaci´on de memoria.
No obstante, nosotros limitaremos nuestro estudio a cinco tipos de datos escalares: int,
unsigned int, float, char y unsigned char. Puedes consultar el resto de tipos escalares en el
ap´endice A. Encontrar´as una variedad enorme: enteros con diferente n´umero de bits, con y sin
signo, flotantes de precisi´on normal y grande, booleanos, etc. Esa enorme variedad es uno de
los puntos fuertes de C, pues permite ajustar el consumo de memoria a las necesidades de cada
programa. En aras de la simplicidad expositiva, no obstante, no la consideraremos en el texto.
1.7.1. El tipo int
El tipo de datos int se usar normalmente para representar n´umeros enteros. La especificaci´on
de C no define el rango de valores que podemos representar con una variable de tipo int, es
decir, no define el n´umero de bits que ocupa una variable de tipo int. No obstante, lo m´as
frecuente es que ocupe 32 bits. Nosotros asumiremos en este texto que el tama˜no de un entero
es de 32 bits, es decir, 4 bytes.
Como los enteros se codifican en complemento a 2, el rango de valores que podemos repre-
sentar es [−2147483648, 2147483647], es decir, [−231
, 231
− 1]. Este rango es suficiente para las
aplicaciones que presentaremos. Si resulta insuficiente o excesivo para alguno de tus programas,
consulta el cat´alogo de tipos que presentamos en el ap´endice A.
En C, tradicionalmente, los valores enteros se han utilizado para codificar valores booleanos.
El valor 0 representa el valor l´ogico ((falso)) y cualquier otro valor representa ((cierto)). En la
´ultima revisi´on de C se ha introducido un tipo booleano, aunque no lo usaremos en este texto
porque, de momento, no es frecuente encontrar programas que lo usen.
1.7.2. El tipo unsigned int
¿Para qu´e desperdiciar el bit m´as significativo en una variable entera de 32 bits que nunca
almacenar´a valores negativos? C te permite definir variables de tipo ((entero sin signo)). El tipo
8Bueno, esos son los que hemos estudiado. Python tiene, adem´as, enteros largos. Otro tipo num´erico no
secuencial de Python es el complejo.
Introducci´on a la Programaci´on con C 23
1.8 Se debe declarar el tipo de toda variable antes de usarla
tiene un nombre compuesto por dos palabras: ((unsigned int)) (aunque la palabra unsigned,
sin m´as, es sin´onimo de unsigned int).
Gracias al aprovechamiento del bit extra es posible aumentar el rango de valores positivos
representables, que pasa a ser [0, 232
− 1], o sea, [0, 4294967295].
1.7.3. El tipo float
El tipo de datos float representa n´umeros en coma flotante de 32 bits. La codificaci´on de coma
flotante permite definir valores con decimales. El m´aximo valor que puedes almacenar en una
variable de tipo float es 3.40282347 · 1038
. Recuerda que el factor exponencial se codifica en los
programas C con la letra ((e)) (o ((E))) seguida del exponente. Ese valor, pues, se codifica as´ı en
un programa C: 3.40282347e38. El n´umero no nulo m´as peque˜no (en valor absoluto) que puedes
almacenar en una variable float es 1.17549435·10−38
(o sea, el literal flotante 1.17549435e-38).
Da la impresi´on, pues, de que podemos representar n´umeros con 8 decimales. No es as´ı: la
precisi´on no es la misma para todos los valores: es tanto mayor cuanto m´as pr´oximo a cero es
el valor.
1.7.4. El tipo char
El tipo char, aunque tenga un nombre que parezca sugerir el t´ermino ((car´acter)) (que en ingl´es
es ((character))) designa en realidad a una variante de enteros: el conjunto de n´umeros que
podemos representar (en complemento a 2) con un solo byte (8 bits). El rango de valores que
puede tomar una variable de tipo char es muy limitado: [−128, 127].
Es frecuente usar variables de tipo char para almacenar caracteres (de ah´ı su nombre)
codificados en ASCII o alguna de sus extensiones (como IsoLatin1). Si una variable a es de
tipo char, la asignaci´on a=’0’ es absolutamente equivalente a la asignaci´on a=48, pues el valor
ASCII del d´ıgito 0 es 48.
1.7.5. El tipo unsigned char
Y del mismo modo que hab´ıa una versi´on para enteros de 32 bits sin signo, hay una versi´on de
char sin signo: unsigned char. Con un unsigned char se puede representar cualquier entero
en el rango [0, 255].
1.8. Se debe declarar el tipo de toda variable antes de usarla
Recuerda que en C toda variable usada en un programa debe declararse antes de ser usada.
Declarar la variable consiste en darle un nombre (identificador) y asignarle un tipo.
1.8.1. Identificadores v´alidos
Las reglas para construir identificadores v´alidos son las mismas que sigue Python: un identifi-
cador es una sucesi´on de letras (del alfabeto ingl´es), d´ıgitos y/o el car´acter de subrayado ( )
cuyo primer car´acter no es un d´ıgito. Y al igual que en Python, no puedes usar una palabra
reservada como identificador. He aqu´ı la relaci´on de palabras reservadas del lenguaje C: 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 y while
1.8.2. Sentencias de declaraci´on
Una variable se declara precediendo su identificador con el tipo de datos de la variable. Este
fragmento, por ejemplo, declara una variable de tipo entero, otra de tipo entero de un byte (o
car´acter) y otra de tipo flotante:
int a;
char b;
float c;
24 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
C, ocupaci´on de los datos, complemento a 2 y portabilidad
Los n´umeros enteros con signo se codifican en complemento a 2. Con n bits puedes repre-
sentar valores enteros en el rango [−2n−1
, 2n−1
− 1]. Los valores positivos se representan
en binario, sin m´as. Los valores negativos se codifican representando en binario su valor
absoluto, invirtiendo todos sus bits y a˜nadiendo 1 al resultado. Sup´on que trabajamos con
datos de tipo char (8 bits). El valor 28 se representa en binario as´ı 00011100. El valor
−28 se obtiene tomando la representaci´on binaria de 28, invirtiendo sus bits (11100011), y
a˜nadiendo uno. El resultado es 11100100.
Una ventaja de la notaci´on en complemento a 2 es que simplifica el dise˜no de circuitos
para la realizaci´on de c´alculos aritm´eticos. Por ejemplo, la resta es una simple suma. Si
deseas restar a 30 el valor 28, basta con sumar 30 y -28 con la misma circuiter´ıa electr´onica
utilizada para efectuar sumas convencionales:
00011110
+ 11100100
00000010
El complemento a 2 puede gastarte malas pasadas si no eres consciente de c´omo fun-
ciona. Por ejemplo, sumar dos n´umeros positivos puede producir un resultado ¡negativo! Si
trabajas con 8 bits y sumas 127 y 1, obtienes el valor −128:
01111111
+ 00000001
10000000
Este fen´omeno se conoce como ((desbordamiento)). C no aborta la ejecuci´on del programa
cuando se produce un desbordamiento: da por bueno el resultado y sigue. Mala cosa: puede
que demos por bueno un programa que est´a produciendo resultados err´oneos.
El est´andar de C no define de modo claro la ocupaci´on de cada uno de sus tipos de datos
lo cual, unido a fen´omenos de desbordamiento, dificulta notablemente la portabilidad de los
programas. En la mayor´ıa de los compiladores y ordenadores actuales, una variable de tipo
int ocupa 32 bits. Sin embargo, en ordenadores m´as antiguos era frecuente que ocupara
s´olo 16. Un programa que suponga una representaci´on mayor que la real puede resultar en
la comisi´on de errores en tiempo de ejecuci´on. Por ejemplo, si una variable a de tipo int
ocupa 32 bits y vale 32767, ejecutar la asignaci´on a = a + 1 almacenar´a en a el valor 32768;
pero si el tipo int ocupa 16 bits, se almacena el valor −32768.
Puede que demos por bueno un programa al compilarlo y ejecutarlo en una plataforma
determinada, pero que falle estrepitosamente cuando lo compilamos y ejecutamos en una
plataforma diferente. O, peor a´un, puede que el error pase inadvertido durante mucho tiem-
po: el programa no abortar´a la ejecuci´on y producir´a resultados incorrectos que podemos
no detectar. Es un problema muy grave.
Los problemas relacionados con la garant´ıa de poder ejecutar un mismo programa en
diferentes plataformas se conocen como problemas de portabilidad. Pese a los muchos pro-
blemas de portabilidad de C, es el lenguaje de programaci´on en el que se ha escrito buena
parte de los programas que hoy ejecutamos en una gran variedad de plataformas.
char, unsigned char, ASCII e IsoLatin1
La tabla ASCII tiene caracteres asociados a valores comprendidos entre 0 y 127, as´ı que
todo car´acter ASCII puede almacenarse en una variable de tipo char. Pero, en realidad,
nosotros no usamos la tabla ASCII ((pura)), sino una extensi´on suya: IsoLatin1 (tambi´en
conocida por ISO-8859-1 o ISO-8859-15, si incluye el s´ımbolo del euro). La tabla IsoLatin1
nos permite utilizar caracteres acentuados y otros s´ımbolos especiales propios de las lenguas
rom´anicas occidentales. ¿Qu´e ocurre si asignamos a una variable de tipo char el car´acter
’´a’? El c´odigo IsoLatin1 de ’´a’ es 225, que es un valor num´erico mayor que 127, el m´aximo
valor entero que podemos almacenar en una variable de tipo char. Mmmm. S´ı, pero 225 se
codifica en binario como esta secuencia de ceros y unos: 11100001. Si interpretamos dicha
secuencia en complemento a dos, tenemos el valor −31, y ese es, precisamente, el valor que
resulta almacenado. Podemos evitar este inconveniente usando el tipo unsigned char,
pues permite almacenar valores entre 0 y 255.
Introducci´on a la Programaci´on con C 25
1.8 Se debe declarar el tipo de toda variable antes de usarla
Se puede declarar una serie de variables del mismo tipo en una sola sentencia de declaraci´on
separando sus identificadores con comas. Este fragmento, por ejemplo, declara tres variables de
tipo entero y otras dos de tipo flotante.
int x, y, z;
float u, v;
En sumatorio.c se declaran tres variables de tipo int, a, b y c, y una de tipo float, s.
Una variable declarada como de tipo entero s´olo puede almacenar valores de tipo entero.
Una vez se ha declarado una variable, es imposible cambiar su tipo, ni siquiera volviendo a
declararla. Este programa, por ejemplo, es incorrecto por el intento de redeclarar el tipo de la
variable a:
redeclara.c E redeclara.c E
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a;
6 float a;
7
8 a = 2;
9 return 0;
10 }
Al compilarlo obtenemos este mensaje de error:
$ gcc redeclara.c -o redeclara
redeclara.c: In function ‘main’:
redeclara.c:6: conflicting types for ‘a’
redeclara.c:5: previous declaration of ‘a’
El compilador nos indica que la variable a presenta un conflicto de tipos en la l´ınea 6 y que
ya hab´ıa sido declarada previamente en la l´ınea 5.
1.8.3. Declaraci´on con inicializaci´on
Debes tener presente que el valor inicial de una variable declarada est´a indefinido. Jam´as debes
acceder al contenido de una variable que no haya sido previamente inicializada. Si lo haces,
el compilador no detectar´a error alguno, pero tu programa presentar´a un comportamiento
indeterminado: a veces funcionar´a bien, y a veces mal, lo cual es peor que un funcionamiento
siempre incorrecto, pues podr´ıas llegar a dar por bueno un programa mal escrito. En esto C
se diferencia de Python: Python abortaba la ejecuci´on de un programa cuando se intentaba
usar una variable no inicializada; C no aborta la ejecuci´on, pero presenta un comportamiento
indeterminado.
Puedes inicializar las variables en el momento de su declaraci´on. Para ello, basta con a˜nadir
el operador de asignaci´on y un valor a continuaci´on de la variable en cuesti´on.
Mira este ejemplo:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a = 2;
6 float b = 2.0, c, d = 1.0, e;
7
8 return 0;
9 }
En ´el, las variables a, b y d se inicializan en la declaraci´on y las variables c y e no tienen valor
definido al ser declaradas.
Recuerda que acceder a variables no inicializadas es una fuente de graves errores. Acost´umbrate
a inicializar las variables tan pronto puedas.
26 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
1.9. Salida por pantalla
La funci´on de impresi´on de informaci´on en pantalla utilizada habitualmente es printf . Es una
funci´on disponible al incluir stdio.h en el programa. El uso de printf es ligeramente m´as com-
plicado que el de la sentencia print de Python, aunque no te resultar´a dif´ıcil si ya has aprendido
a utilizar el operador de formato en Python (%).
En su forma de uso m´as simple, printf permite mostrar una cadena por pantalla.
1 #include <stdio.h>
2
3 int main(void)
4 {
5 printf ("Una cadena");
6 printf ("y otra.");
7 return 0;
8 }
La funci´on printf no a˜nade un salto de l´ınea autom´aticamente, como s´ı hac´ıa print en Python.
En el programa anterior, ambas cadenas se muestran una a continuaci´on de otra. Si deseas que
haya un salto de l´ınea, deber´as escribir n al final de la cadena.
1 #include <stdio.h>
2
3 int main(void)
4 {
5 printf ("Una cadenan");
6 printf ("y otra.n");
7 return 0;
8 }
1.9.1. Marcas de formato para la impresi´on de valores con printf
Marcas de formato para n´umeros
Para mostrar n´umeros enteros o flotantes has de usar necesariamente cadenas con formato.
Afortunadamente, las marcas que aprendiste al estudiar Python se utilizan en C. Eso s´ı, hay
algunas que no te hemos presentado a´un y que tambi´en se recogen en esta tabla:
Tipo Marca
int %d
unsigned int %u
float %f
char %hhd
unsigned char %hhu
Por ejemplo, si a es una variable de tipo int con valor 5, b es una variable de tipo float con
valor 1.0, y c es una variable de tipo char con valor 100, esta llamada a la funci´on printf :
printf ("Un entero: %d, un flotante: %f, un byte: %hhdn", a, b, c);
muestra por pantalla esto:
Un entero: 5, un flotante: 1.000000, un byte: 100
¡Ojo! a la cadena de formato le sigue una coma, y no un operador de formato como suced´ıa
en Python. Cada variable se separa de las otras con una coma.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 15 ¿Que mostrar´a por pantalla esta llamada a printf suponiendo que a es de tipo entero y
vale 10?
printf ("%d-%dn", a+1, 2+2);
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Introducci´on a la Programaci´on con C 27
1.9 Salida por pantalla
Las marcas de formato para enteros aceptan modificadores, es decir, puedes alterar la repre-
sentaci´on introduciendo ciertos caracteres entre el s´ımbolo de porcentaje y el resto de la marca.
Aqu´ı tienes los principales:
Un n´umero positivo: reserva un n´umero de espacios determinado (el que se indique) para
representar el valor y muestra el entero alineado a la derecha.
Ejemplo: la sentencia
printf ("[%6d]", 10);
muestra en pantalla:
[ 10]
Un n´umero negativo: reserva tantos espacios como indique el valor absoluto del n´umero
para representar el entero y muestra el valor alineado a la izquierda.
Ejemplo: la sentencia
printf ("[%-6d]", 10);
muestra en pantalla:
[10 ]
Un n´umero que empieza por cero: reserva tantos espacios como indique el n´umero para
representar el entero y muestra el valor alineado a la derecha. Los espacios que no ocupa
el entero se rellenan con ceros.
Ejemplo: la sentencia
printf ("[%06d]", 10);
muestra en pantalla:
[000010]
El signo +: muestra expl´ıcitamente el signo (positivo o negativo) del entero.
Ejemplo: la sentencia
printf ("[%+6d]", 10);
muestra en pantalla:
[ +10]
Hay dos notaciones alternativas para la representaci´on de flotantes que podemos seleccionar
mediante la marca de formato adecuada:
Tipo Notaci´on Marca
float Convencional %f
float Cient´ıfica %e
La forma convencional muestra los n´umeros con una parte entera y una decimal separadas
por un punto. La notaci´on cient´ıfica representa al n´umero como una cantidad con una sola
cifra entera y una parte decimal, pero seguida de la letra ((e)) y un valor entero. Por ejemplo, en
notaci´on cient´ıfica, el n´umero 10.1 se representa con 1.010000e+01 y se interpreta as´ı: 1.01×101
.
Tambi´en puedes usar modificadores para controlar la representaci´on en pantalla de los flotan-
tes. Los modificadores que hemos presentado para los enteros son v´alidos aqu´ı. Tienes, adem´as,
la posibilidad de fijar la precisi´on:
Un punto seguido de un n´umero: indica cu´antos decimales se mostrar´an.
Ejemplo: la sentencia
printf ("[%6.2f]", 10.1);
muestra en pantalla:
[ 10.10]
28 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
Marcas de formato para texto
Y a´un nos queda presentar las marcas de formato para texto. C distingue entre caracteres y
cadenas:
Tipo Marca
car´acter %c
cadena %s
¡Atenci´on! La marca %c muestra como car´acter un n´umero entero. Naturalmente, el car´acter
que se muestra es el que corresponde al valor entero seg´un la tabla ASCII (o, en tu ordenador,
IsoLatin1 si el n´umero es mayor que 127). Por ejemplo, la sentencia
printf ("[%c]", 97);
muestra en pantalla:
[a]
Recuerda que el valor 97 tambi´en puede representarse con el literal ’a’, as´ı que esta otra
sentencia
printf ("[%c]", ’a’);
tambi´en muestra en pantalla esto:
[a]
A´un no sabemos almacenar cadenas en variables, as´ı que poca aplicaci´on podemos encontrar
de momento a la marca %s. He aqu´ı, de todos modos, un ejemplo trivial de uso:
printf ("[%s]", "una cadena");
En pantalla se muestra esto:
[una cadena]
Tambi´en puedes usar n´umeros positivos y negativos como modificadores de estas marcas.
Su efecto es reservar los espacios que indiques y alinear a derecha o izquierda.
Aqu´ı tienes un programa de ejemplo en el que se utilizan diferentes marcas de formato con
y sin modificadores.
modificadores.c modificadores.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 char c = ’a’;
6 int i = 1000000;
7 float f = 2e1;
8
9 printf ("c : %c %hhd <-
!
IMPORTANTE! Estudia la diferencia.n", c, c);
10 printf ("i : %d |%10d|%-10d|n", i, i, i);
11 printf ("f : %f |%10.2f|%+4.2f|n", f, f, f);
12 return 0;
13 }
El resultado de ejecutar el programa es la impresi´on por pantalla del siguiente texto:
c : a 97 <-
!
IMPORTANTE! Estudia la diferencia.
i : 1000000 | 1000000|1000000 |
f : 20.000000 | 20.00|+20.00|
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 16 ¿Qu´e muestra por pantalla cada uno de estos programas?
Introducci´on a la Programaci´on con C 29
1.9 Salida por pantalla
a) ascii1.c ascii1.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 char i;
6 for (i=’A’; i<=’Z’; i++)
7 printf ("%c", i);
8 printf ("n");
9 return 0;
10 }
b) ascii2.c ascii2.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 char i;
6 for (i=65; i<=90; i++)
7 printf ("%c", i);
8 printf ("n");
9 return 0;
10 }
c) ascii3.c ascii3.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i;
6 for (i=’A’; i<=’Z’; i++)
7 printf ("%d ", i);
8 printf ("n");
9 return 0;
10 }
d) ascii4.c ascii4.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i;
6 for (i=’A’; i<=’Z’; i++)
7 printf ("%d-%c ", i, i);
8 printf ("n");
9 return 0;
10 }
e) ascii5.c ascii5.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 char i;
6 for (i=’A’; i<=’z’; i++) // Ojo: la z es min´uscula.
7 printf ("%d ", (int) i);
8 printf ("n");
9 return 0;
10 }
· 17 Dise˜na un programa que muestre la tabla ASCII desde su elemento de c´odigo num´erico
32 hasta el de c´odigo num´erico 126. En la tabla se mostrar´an los c´odigos ASCII, adem´as de
las respectivas representaciones como caracteres de sus elementos. Aqu´ı tienes las primeras y
30 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
´ultimas l´ıneas de la tabla que debes mostrar (debes hacer que tu programa muestre la informa-
ci´on exactamente como se muestra aqu´ı):
+---------+----------+
| Decimal | Car´acter |
+---------+----------+
| 32 | |
| 33 | ! |
| 34 | " |
| 35 | # |
| 36 | $ |
| 37 | % |
... ...
| 124 | | |
| 125 | } |
| 126 | ~ |
+---------+----------+
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Hay un rico juego de marcas de formato y las recogemos en el ap´endice A. Cons´ultalo si
usas tipos diferentes de los que presentamos en el texto o si quieres mostrar valores enteros en
base 8 o 16. En cualquier caso, es probable que necesites conocer una marca especial, %%, que
sirve para mostrar el s´ımbolo de porcentaje. Por ejemplo, la sentencia
printf ("[%d%%]", 100);
muestra en pantalla:
[100%]
1.10. Variables y direcciones de memoria
Antes de presentar con cierto detalle la entrada de datos por teclado mediante scanf , nos
conviene detenernos brevemente para estudiar algunas cuestiones relativas a las variables y la
memoria que ocupan.
Recuerda que la memoria es una sucesi´on de celdas numeradas y que una direcci´on de
memoria no es m´as que un n´umero entero. La declaraci´on de una variable supone la reserva de
una zona de memoria lo suficientemente grande para albergar su contenido. Cuando declaramos
una variable de tipo int, por ejemplo, se reservan 4 bytes de memoria en los que se almacenar´a
(codificado en complemento a 2) el valor de dicha variable. Modificar el valor de la variable
mediante una asignaci´on supone modificar el patr´on de 32 bits (4 bytes) que hay en esa zona
de memoria.
Este programa, por ejemplo,
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b;
6
7 a = 0;
8 b = a + 8;
9
10 return 0;
11 }
reserva 8 bytes para albergar dos valores enteros.9
Imagina que a ocupa los bytes 1000–1003 y
b ocupa los bytes 1004–1007. Podemos representar la memoria as´ı:
9En el apartado 3.5.2 veremos que la reserva se produce en una zona de memoria especial llamada pila. No
conviene que nos detengamos ahora a considerar los matices que ello introduce en el discurso.
Introducci´on a la Programaci´on con C 31
1.10 Variables y direcciones de memoria
996:
1000:
1004:
1008:
a
b
01010010 10101000 01110011 11110010
01011010 00111101 00111010 11010111
10111011 10010110 01010010 01010011
11010111 01000110 11110010 01011101
Observa que, inicialmente, cuando se reserva la memoria, ´esta contiene un patr´on de bits
arbitrario. La sentencia a = 0 se interpreta como ((almacena el valor 0 en la direcci´on de memoria
de a)), es decir, ((almacena el valor 0 en la direcci´on de memoria 1000))10
. Este es el resultado
de ejecutar esa sentencia:
996:
1000:
1004:
1008:
a
b
01010010 10101000 01110011 11110010
00000000 00000000 00000000 00000000
10111011 10010110 01010010 01010011
11010111 01000110 11110010 01011101
La asignaci´on b = a + 8 se interpreta como ((calcula el valor que resulta de sumar 8 al contenido
de la direcci´on de memoria 1000 y deja el resultado en la direcci´on de memoria 1004)).
996:
1000:
1004:
1008:
a
b
01010010 10101000 01110011 11110010
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00001000
11010111 01000110 11110010 01011101
Hemos supuesto que a est´a en la direcci´on 1000 y b en la 1004, pero ¿podemos saber en qu´e
direcciones de memoria se almacenan realmente a y b? S´ı: el operador & permite conocer la
direcci´on de memoria en la que se almacena una variable:
direcciones.c direcciones.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b;
6
7 a = 0;
8 b = a + 8;
9
10 printf ("Direcci´on de a: %un", (unsigned int)&a);
11 printf ("Direcci´on de b: %un", (unsigned int)&b);
12
13 return 0;
14 }
Observa que usamos la marca de formato %u para mostrar el valor de la direcci´on de memoria,
pues debe mostrarse como entero sin signo. La conversi´on a tipo unsigned int evita molestos
mensajes de aviso al compilar.11
Al ejecutar el programa tenemos en pantalla el siguiente texto (puede que si ejecutas t´u
mismo el programa obtengas un resultado diferente):
10En realidad, en la zona de memoria 1000–1003, pues se modifica el contenido de 4 bytes. En aras de la
brevedad, nos referiremos a los 4 bytes s´olo con la direcci´on del primero de ellos.
11Hay un marca especial, %p, que muestra directamente la direcci´on de memoria sin necesidad de efectuar la
conversi´on a unsigned int, pero lo hace usando notaci´on hexadecimal.
32 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
Direcci´on de a: 3221222580
Direcci´on de b: 3221222576
O sea, que en realidad este otro gr´afico representa mejor la disposici´on de las variables en
memoria:
3221222572:
3221222576:
3221222580:
3221222584:
b
a
01010010 10101000 01110011 11110010
00000000 00000000 00000000 00001000
00000000 00000000 00000000 00000000
11010111 01000110 11110010 01011101
Normalmente no necesitamos saber en qu´e direcci´on de memoria se almacena una variable,
as´ı que no recurriremos a representaciones gr´aficas tan detalladas como las que hemos presen-
tado. Usualmente nos conformaremos con representar las variables escalares mediante cajas y
representaremos su valor de una forma m´as c´omodamente legible que como una secuencia de
bits. La representaci´on anterior se simplificar´a, pues, as´ı:
0a
8b
Las direcciones de memoria de las variables se representar´an con flechas que apuntan a sus
correspondientes cajas:
&a
0a
&b
8b
Ahora que hemos averiguado nuevas cosas acerca de las variables, vale la pena que reflexio-
nemos brevemente sobre el significado de los identificadores de variables all´ı donde aparecen.
Considera este sencillo programa:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b;
6
7 a = 0;
8 b = a;
9 scanf ("%d", &b);
10 a = a + b;
11
12 return 0;
13 }
¿C´omo se interpreta la sentencia de asignaci´on a = 0? Se interpreta como ((almacena el valor
0 en la direcci´on de memoria de a)). ¿Y b = a?, ¿c´omo se interpreta? Como ((almacena una
copia del contenido de a en la direcci´on de memoria de b)). F´ıjate bien, el identificador a recibe
interpretaciones diferentes seg´un aparezca a la izquierda o a la derecha de una asignaci´on:
a la izquierda del igual, significa ((la direcci´on de a)),
y a la derecha, es decir, en una expresi´on, significa ((el contenido de a)).
La funci´on scanf necesita una direcci´on de memoria para saber d´onde debe depositar un
resultado. Como no estamos en una sentencia de asignaci´on, sino en una expresi´on, es necesario
Introducci´on a la Programaci´on con C 33
1.11 Entrada por teclado
que obtengamos expl´ıcitamente la direcci´on de memoria con el operador &b. As´ı, para leer por
teclado el valor de b usamos la llamada scanf ("%d", &b).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 18 Interpreta el significado de la sentencia a = a + b.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.11. Entrada por teclado
La funci´on scanf , disponible al incluir stdio.h, permite leer datos por teclado. La funci´on scanf
se usa de un modo similar a printf : su primer argumento es una cadena con marcas de formato.
A ´este le siguen una o m´as direcciones de memoria. Si deseas leer por teclado el valor de una
variable entera a, puedes hacerlo as´ı:
scanf ("%d", &a);
Observa que la variable cuyo valor se lee por teclado va obligatoriamente precedida por el
operador &: es as´ı como obtenemos la direcci´on de memoria en la que se almacena el valor de
la variable. Uno de los errores que cometer´as con mayor frecuencia es omitir el car´acter & que
debe preceder a todas las variables escalares en scanf .
Recuerda: la funci´on scanf recibe estos datos:
Una cadena cuya marca de formato indica de qu´e tipo es el valor que vamos a leer por
teclado:
Tipo Marca
int %d
unsigned int %u
float %f
char como entero %hhd
char como car´acter %c
unsigned char como entero %hhu
unsigned char como car´acter %c
La direcci´on de memoria que corresponde al lugar en el que se depositar´a el valor le´ıdo.
Debemos proporcionar una direcci´on de memoria por cada marca de formato indicada en
el primero argumento.
Observa que hay dos formas de leer un dato de tipo char o unsigned char: como entero (de
un byte con o sin signo, respectivamente) o como car´acter. En el segundo caso, se espera que el
usuario teclee un solo car´acter y se almacenar´a en la variable su valor num´erico seg´un la tabla
ASCII o su extensi´on IsoLatin.
Una advertencia: la lectura de teclado en C presenta numerosas dificultades pr´acticas. Es
muy recomendable que leas el ap´endice B antes de seguir estudiando y absolutamente necesario
que lo leas antes de empezar a practicar con el ordenador. Si no lo haces, muchos de tus
programas presentar´an un comportamiento muy extra˜no y no entender´as por qu´e. T´u mismo.
1.12. Expresiones
Muchos de los s´ımbolos que representan a los operadores de Python que ya conoces son los mis-
mos en C. Los presentamos ahora agrupados por familias. (Consulta los niveles de precedencia
y asociatividad en la tabla de la p´agina 40.) Presta especial atenci´on a los operadores que no
conoces por el lenguaje de programaci´on Python, como son los operadores de bits, el operador
condicional o los de incremento/decremento.
Operadores aritm´eticos Suma (+), resta (-), producto (*), divisi´on (/), m´odulo o resto de
la divisi´on (%), identidad (+ unario), cambio de signo (- unario).
No hay operador de exponenciaci´on.12
12Pero hay una funci´on de la biblioteca matem´atica que permite calcular la potencia de un n´umero: pow.
34 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
Errores frecuentes en el uso de scanf
Es responsabilidad del programador pasar correctamente los datos a scanf . Un error que
puede tener graves consecuencias consiste en pasar incorrectamente la direcci´on de memoria
en la que dejar´a el valor le´ıdo. Este programa, por ejemplo, es err´oneo:
scanf ("%d", a);
La funci´on scanf no est´a recibiendo la direcci´on de memoria en la que ((reside)) a, sino el
valor almacenado en a. Si scanf interpreta dicho valor como una direcci´on de memoria (cosa
que hace), guardar´a en ella el n´umero que lea de teclado. ¡Y el compilador no necesariamente
detectar´a el error! El resultado es catastr´ofico.
Otro error t´ıpico al usar scanf consiste en confundir el tipo de una variable y/o la marca
de formato que le corresponde. Por ejemplo, imagina que c es una variable de tipo char.
Este intento de lectura de su valor por teclado es err´oneo:
scanf ("%d", &c);
A scanf le estamos pasando la direcci´on de memoria de la variable c. Hasta ah´ı, bien. Pero
c s´olo ocupa un byte y a scanf le estamos diciendo que ((rellene)) 4 bytes con un n´umero
entero a partir de esa direcci´on de memoria. Otro error de consecuencias grav´ısimas. La
marca de formato adecuada para leer un n´umero de tipo char hubiera sido %hhd.
scanf ("%hhd", &c);
La divisi´on de dos n´umeros enteros proporciona un resultado de tipo entero (como ocurr´ıa
en Python).
Los operadores aritm´eticos s´olo funcionan con datos num´ericos13
. No es posible, por ejem-
plo, concatenar cadenas con el operador + (cosa que s´ı pod´ıamos hacer en Python).
La dualidad car´acter-entero del tipo char hace que puedas utilizar la suma o la resta
(o cualquier otro operador aritm´etico) con variables o valores de tipo char. Por ejemplo
’a’ + 1 es una expresi´on v´alida y su valor es ’b’ (o, equivalentemente, el valor 98, ya que
’a’ equivale a 97). (Recuerda, no obstante, que un car´acter no es una cadena en C, as´ı
que "a" + 1 no es "b".)
Operadores l´ogicos Negaci´on o no-l´ogica (!), y-l´ogica o conjunci´on (&&) y o-l´ogica o disyun-
ci´on (||).
Los s´ımbolos son diferentes de los que aprendimos en Python. La negaci´on era all´ı not,
la conjunci´on era and y la disyunci´on or.
C sigue el convenio de que 0 significa falso y cualquier otro valor significa cierto. As´ı pues,
cualquier valor entero puede interpretarse como un valor l´ogico, igual que en Python.
Operadores de comparaci´on Igual que (==), distinto de (!=), menor que (<), mayor que (>),
menor o igual que (<=), mayor o igual que (>=).
Son viejos conocidos. Una diferencia con respecto a Python: s´olo puedes usarlos para
comparar valores escalares. No puedes, por ejemplo, comparar cadenas mediante estos
operadores.
La evaluaci´on de una comparaci´on proporciona un valor entero: 0 si el resultado es falso
y cualquier otro si el resultado es cierto (aunque normalmente el valor para cierto es 1).
Operadores de bits Complemento (~), ((y)) (&), ((o)) (|), ((o)) exclusiva (^), desplazamiento a
izquierdas (<<), desplazamiento a derechas (>>).
Estos operadores trabajan directamente con los bits que codifican un valor entero. Aunque
tambi´en est´an disponibles en Python, no los estudiamos entonces porque son de uso
infrecuente en ese lenguaje de programaci´on.
13Y la suma y la resta trabajan tambi´en con punteros. Ya estudiaremos la denominada ((aritm´etica de punteros))
m´as adelante.
Introducci´on a la Programaci´on con C 35
1.12 Expresiones
-Wall
Cuando escribimos un texto en castellano podemos cometer tres tipos de errores:
Errores l´exicos: escribimos palabras incorrectamente, con errores ortogr´aficos, o usa-
mos palabras inexistentes. Por ejemplo: ((herror)), ((l´ecsico)), ((jerig´ondor)).
Errores sint´acticos: aunque las palabras son v´alidas y est´an correctamente escritas,
faltan componentes de una frase (como el sujeto o el verbo), no hay concordancia
entre componentes de la frase, los componentes de la frase no ocupan la posici´on ade-
cuada, etc. Por ejemplo: ((el error sint´actica son)), ((la compilador detect´o
errores)).
Errores sem´anticos: la frase est´a correctamente construida pero carece de significado
v´alido en el lenguaje. Por ejemplo: ((el compilador silb´o una tonada en v´ıdeo)),
((los osos son enteros con decimales romos)).
Lo mismo ocurre con los programas C; pueden contener errores de los tres tipos:
Errores l´exicos: usamos car´acteres no v´alidos o construimos incorrectamente compo-
nentes elementales del programa (como identificadores, cadenas, palabras clave, etc.).
Por ejemplo: ((@3)), (("una cadena sin cerrar)).
Errores sint´acticos: construimos mal una sentencia aunque usamos palabras v´alidas.
Por ejemplo: ((while a < 10 { a += 1; })), ((b = 2 * / 3;)).
Errores sem´anticos: la sentencia no tiene un significado ((v´alido)). Por ejemplo, si a
es de tipo float, estas sentencias contienen errores sem´anticos: ((scanf ("%d", &a);))
(se trata de leer el valor de a como si fuera un entero), ((if (a = 1.0) { a = 2.0; }))
(no se est´a comparando el valor de a con 1.0, sino que se asigna el valor 1.0 a a).
El compilador de C no deja pasar un solo error l´exico o sint´actico: cuando lo detecta, nos
informa del error y no genera traducci´on a c´odigo de m´aquina del programa. Con los errores
sem´anticos, sin embargo, el compilador es m´as indulgente: la filosof´ıa de C es suponer que
el programador puede tener una buena raz´on para hacer algunas de las cosas que expresa
en los programas, aunque no siempre tenga un significado ((correcto)) a primera vista. No
obstante, y para seg´un qu´e posibles errores, el compilador puede emitir avisos (warnings).
Es posible regular hasta qu´e punto deseamos que el compilador nos proporcione avisos.
La opci´on -Wall (((Warning all)), que significa ((todos los avisos))) activa la detecci´on de
posibles errores sem´anticos, notific´andolos como avisos. Este programa err´oneo, por ejemplo,
no genera ning´un aviso al compilarse sin -Wall:
semanticos.c E semanticos.c E
1 #include <stdio.h>
2 int main(void)
3 {
4 float a;
5 scanf ("%d", &a);
6 if (a = 0.0) { a = 2.0; }
7 return 0;
8 }
Pero si lo compilas con ((gcc -Wall semanticos.c -o semanticos)), aparecen avisos
(warnings) en pantalla:
$ gcc -Wall semanticos.c -o semanticos
semanticos.c: In function ‘main’:
semanticos.c:5: warning: int format, float arg (arg 2)
semanticos.c:6: warning: suggest parentheses around assignment used as
truth value
El compilador advierte de errores sem´anticos en las l´ıneas 5 y 6. Te har´a falta bastante
pr´actica para aprender a descifrar mensajes tan parcos o extra˜nos como los que produce
gcc, as´ı que conviene que te acostumbres a compilar con -Wall. (Y hazlo siempre que
tu programa presente un comportamiento an´omalo y no hayas detectado errores l´exicos o
sint´acticos.)
El operador de complemento es unario e invierte todos los bits del valor. Tanto & como |
36 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
Lecturas m´ultiples con scanf
No te hemos contado todo sobre scanf . Puedes usar scanf para leer m´as de un valor. Por
ejemplo, este programa lee dos valores enteros con un solo scanf :
lectura multiple.c lectura multiple.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b;
6 printf ("Introduce dos enteros: ");
7 scanf ("%d %d", &a, &b);
8 printf ("Valores le´ıdos: %d y %dn", a, b);
9 return 0;
10 }
Tambi´en podemos especificar con cierto detalle c´omo esperamos que el usuario introduzca
la informaci´on. Por ejemplo, con scanf ("%d-%d", &a, &b) indicamos que el usuario de-
be separar los enteros con un gui´on; y con scanf ("(%d,%d)", &a, &b) especificamos que
esperamos encontrar los enteros encerrados entre par´entesis y separados por comas.
Lee la p´agina de manual de scanf (escribiendo man 3 scanf en el int´erprete de ´ordenes
Unix) para obtener m´as informaci´on.
y ^ son operadores binarios. El operador & devuelve un valor cuyo n-´esimo bit es 1 si y
s´olo si los dos bits de la n-´esima posici´on de los operandos son tambi´en 1. El operador |
devuelve 0 en un bit si y solo si los correspondientes bits en los operandos son tambi´en
0. El operador ^ devuelve 1 si y s´olo si los correspondientes bits en los operandos son
diferentes. Lo entender´as mejor con un ejemplo. Imagina que a y b son variables de tipo
char que valen 6 y 3, respectivamente. En binario, el valor de a se codifica como 00000110
y el valor de b como 00000011. El resultado de a | b es 7, que corresponde al valor en
base diez del n´umero binario 000000111. El resultado de a & b es, en binario, 000000010,
es decir, el valor decimal 2. El resultado binario de a ^ b es 000000101, que en base 10
es 5. Finalmente, el resultado de ~a es 11111001, es decir, −7 (recuerda que un n´umero
con signo est´a codificado en complemento a 2, as´ı que si su primer bit es 1, el n´umero es
negativo).
Los operadores de desplazamiento desplazan los bits un n´umero dado de posiciones a
izquierda o derecha. Por ejemplo, 16 como valor de tipo char es 00010000, as´ı que 16 << 1
es 32, que en binario es 00100000, y 16 >> 1 es 8, que en binario es 00001000.
Operadores de bits y programaci´on de sistemas
C presenta una enorme colecci´on de operadores, pero quiz´a los que te resulten m´as llamativos
sean los operadores de bits. Dif´ıcilmente los utilizar´as en programas convencionales, pero
son insustituibles en la programaci´on de sistemas. Cuando manejes informaci´on a muy bajo
nivel es probable que necesites acceder a bits y modificar sus valores.
Por ejemplo, el control de ciertos puertos del ordenador pasa por leer y asignar valores
concretos a ciertos bits de direcciones virtuales de memoria. Puede que poner a 1 el bit
menos significativo de determinada direcci´on permita detener la actividad de una impresora
conectada a un puerto paralelo, o que el bit m´as significativo nos alerte de si falta papel en
la impresora.
Si deseas saber si un bit est´a o no activo, puedes utilizar los operadores & y <<. Para saber,
por ejemplo, si el octavo bit de una variable x est´a activo, puedes calcular x & (1 << 7). Si
el resultado es cero, el bit no est´a activo; en caso contrario, est´a activo. Para fijar a 1 el
valor de ese mismo bit, puedes hacer x = x | (1 << 7).
Los operadores de bits emulan el comportamiento de ciertas instrucciones disponibles
en los lenguajes ensambladores. La facilidad que proporciona C para escribir programas de
((bajo nivel)) es grande, y por ello C se considera el lenguaje a elegir cuando hemos de escribir
un controlador para un dispositivo o el c´odigo de un sistema operativo.
Introducci´on a la Programaci´on con C 37
1.12 Expresiones
Operadores de asignaci´on Asignaci´on (=), asignaci´on con suma (+=), asignaci´on con resta
(-=), asignaci´on con producto (*=), asignaci´on con divisi´on (/=), asignaci´on con m´odulo
(%=), asignaci´on con desplazamiento a izquierda (<<=), asignaci´on con desplazamiento
a derecha (>>=), asignaci´on con ((y)) (&=), asignaci´on con ((o)) (|=), asignaci´on con ((o))
exclusiva (^=).
Puede resultarte extra˜no que la asignaci´on se considere tambi´en un operador. Que sea un
operador permite escribir asignaciones m´ultiples como ´esta:
a = b = 1;
Es un operador asociativo por la derecha, as´ı que las asignaciones se ejecutan en este
orden:
a = (b = 1);
El valor que resulta de evaluar una asignaci´on con = es el valor asignado a su parte
izquierda. Cuando se ejecuta b = 1, el valor asignado a b es 1, as´ı que ese valor es el que
se asigna tambi´en a a.
La asignaci´on con una operaci´on ((op)) hace que a la variable de la izquierda se le asigne
el resultado de operar con ((op)) su valor con el operando derecho. Por ejemplo, a /= 3 es
equivalente a a = a / 3.
Este tipo de asignaci´on con operaci´on recibe el nombre de asignaci´on aumentada.
Operador de tama˜no sizeof.
El operador sizeof puede aplicarse a un nombre de tipo (encerrado entre par´entesis) o
a un identificador de variable. En el primer caso devuelve el n´umero de bytes que ocupa
en memoria una variable de ese tipo, y en el segundo, el n´umero de bytes que ocupa esa
variable. Si a es una variable de tipo char, tanto sizeof(a) como sizeof(char) devuelven
el valor 1. Ojo: recuerda que ’a’ es literal entero, as´ı que sizeof(’a’) vale 4.
Operadores de coerci´on o conversi´on de tipos (en ingl´es ((type casting operator))). Pue-
des convertir un valor de un tipo de datos a otro que sea ((compatible)). Para ello dispones
de operadores de la forma (tipo), donde tipo es int, float, etc.
Por ejemplo, si deseas efectuar una divisi´on entre enteros que no pierda decimales al
convertir el resultado a un flotante, puedes hacerlo como te muestra este programa:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 float x;
6 int a = 1, b = 2;
7
8 x = a / (float) b;
9 }
En este ejemplo, hemos convertido el valor de b a un float antes de efectuar la divisi´on.
Es similar a la funci´on float de Python, s´olo que en Python se hac´ıa la conversi´on con
una llamada a funci´on como float(b), y aqu´ı utilizamos un operador prefijo: (float) b. Es
una notaci´on bastante extra˜na, as´ı que es probable que te confunda durante un tiempo.
En la siguiente secci´on abundaremos en la cuesti´on de la conversi´on de tipos en C.
Operador condicional (?:).
Este operador no tiene correlato en Python. Hay tres operandos: una condici´on y dos ex-
presiones14
. El resultado de la operaci´on es el valor de la primera expresi´on si la condici´on
es cierta y el valor de la segunda si es falsa. Por ejemplo, la asignaci´on
a = (x > 10) ? 100 : 200
14Lo cierto es que hay tres expresiones, pues la comparaci´on no es m´as que una expresi´on. Si dicha expresi´on
devuelve el valor 0, se interpreta el resultado como ((falso)); en caso contrario, el resultado es ((cierto)).
38 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
almacena en a el valor 100 o 200, dependiendo de si x es o no es mayor que 10. Es
equivalente a este fragmento de programa:
if (x > 10)
a = 100;
else
a = 200;
Operadores de incremento/decremento Preincremento (++ en forma prefija), postincre-
mento (++ en forma postfija), predecremento (-- en forma prefija), postdecremento (--
en forma postfija).
Estos operadores no tienen equivalente inmediato en Python. Los operadores de incre-
mento y decremento pueden ir delante de una variable (forma prefija) o detr´as (forma
postfija). La variable debe ser de tipo entero (int, unsigned int, char, etc.). En ambos
casos incrementan (++) o decrementan (--) en una unidad el valor de la variable entera.
Si i vale 1, valdr´a 2 despu´es de ejecutar ++i o i++, y valdr´a 0 despu´es de ejecutar --i o i--.
Hay una diferencia importante entre aplicar estos operadores en forma prefija o sufija.
La expresi´on i++ primero se eval´ua como el valor actual de i y despu´es hace que i
incremente su valor en una unidad.
La expresi´on ++i primero incrementa el valor de i en una unidad y despu´es se eval´ua
como el valor actual (que es el que resulta de efectuar el incremento).
Si el operador se est´a aplicando en una expresi´on, esta diferencia tiene importancia. Su-
pongamos que i vale 1 y que evaluamos esta asignaci´on:
a = i++;
La variable a acaba valiendo 1 e i acaba valiendo 2. F´ıjate: al ser un postincremento,
primero se devuelve el valor de i, que se asigna a a, y despu´es se incrementa i.
Al ejecutar esta otra asignaci´on obtenemos un resultado diferente:
a = ++i;
Tanto a como i acaban valiendo 2. El operador de preincremento primero asigna a i su
valor actual incrementado en una unidad y despu´es devuelve ese valor (ya incrementado),
que es lo que finalmente estamos asignando a a.
Lo mismo ocurre con los operadores de pre y postdecremento, pero, naturalmente, decre-
mentado el valor en una unidad en lugar de incrementarlo.
Que haya operadores de pre y postincremento (y pre y postdecremento) te debe parecer
una rareza excesiva y pensar´as que nunca necesitar´as hilar tan fino. Si es as´ı, te equivocas:
en los pr´oximos cap´ıtulos usaremos operadores de incremento y necesitaremos escoger
entre preincremento y postincremento.
Nos dejamos en el tintero unos pocos operadores (((())), (([])), ((->)), ((.)), ((,)), y ((*)) unario.
Los presentaremos cuando convenga y sepamos algo m´as de C.
C++
Ya debes entender de d´onde viene el nombre C++: es un C ((incrementado)), o sea, mejorado.
En realidad C++ es mucho m´as que un C con algunas mejoras: es un lenguaje orientado a
objetos, as´ı que facilita el dise˜no de programas siguiendo una filosof´ıa diferente de la propia
de los lenguajes imperativos y procedurales como C. Pero esa es otra historia.
En esta tabla te relacionamos todos los operadores (incluso los que a´un no te hemos presen-
tado con detalle) ordenados por precedencia (de mayor a menor) y con su aridad (n´umero de
operandos) y asociatividad:
Introducci´on a la Programaci´on con C 39
1.12 Expresiones
Operador Aridad Asociatividad
() [] -> . ++postfijo--postfijo 2 izquierda
! ~ + - sizeof * & (tipo) ++prefijo--prefijo 1 derecha
* / % 2 izquierda
+ - 2 izquierda
<< >> 2 izquierda
< <= > >= 2 izquierda
== != 2 izquierda
& 2 izquierda
^ 2 izquierda
| 2 izquierda
&& 2 izquierda
|| 2 izquierda
?: 3 izquierda
= += -= *= /= %= <<= >>= &= ^= |= 2 derecha
, 2 izquierda
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 19 Sean a, b y c tres variables de tipo int cuyos valores actuales son 0, 1 y 2, respectivamente.
¿Qu´e valor tiene cada variable tras ejecutar esta secuencia de asignaciones?
1 a = b++ - c--;
2 a += --b;
3 c *= a + b;
4 a = b | c;
5 b = (a > 0) ? ++a : ++c;
6 b <<= a = 2;
7 c >>= a == 2;
8 a += a = b + c;
· 20 ¿Qu´e hace este programa?
ternario.c ternario.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b, c, r;
6
7 printf ("Dame un valor entero: "); scanf ("%d", &a);
8 printf ("Dame otro valor entero: "); scanf ("%d", &b);
9 printf ("Y uno m´as: "); scanf ("%d", &c);
10
11 r = (a < b) ? ( (a < c) ? a : c ) : ( (b < c) ? b : c );
12
13 printf ("Resultado: %dn", r);
14
15 return 0;
16 }
· 21 Haz un programa que solicite el valor de x y muestre por pantalla el resultado de evaluar
x4
− x2
+ 1. (Recuerda que en C no hay operador de exponenciaci´on.)
· 22 Dise˜na un programa C que solicite la longitud del lado de un cuadrado y muestre por
pantalla su per´ımetro y su ´area.
· 23 Dise˜na un programa C que solicite la longitud de los dos lados de un rect´angulo y
muestre por pantalla su per´ımetro y su ´area.
· 24 Este programa C es problem´atico:
un misterio.c un misterio.c
1 #include <stdio.h>
40 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
2
3 int main(void)
4 {
5 int a, b;
6
7 a = 2147483647;
8 b = a + a;
9 printf ("%dn", a);
10 printf ("%dn", b);
11 return 0;
12 }
Al compilarlo y ejecutarlo hemos obtenido la siguiente salida por pantalla:
2147483647
-2
¿Qu´e ha ocurrido?
· 25 Dise˜na un programa C que solicite el radio r de una circunferencia y muestre por
pantalla su per´ımetro (2πr) y su ´area (πr2
).
· 26 Si a es una variable de tipo char con el valor 127, ¿qu´e vale ~a? ¿Y qu´e vale !a? Y si a
es una variable de tipo unsigned int con el valor 2147483647, ¿qu´e vale ~a? ¿Y qu´e vale !a?
· 27 ¿Qu´e resulta de evaluar cada una de estas dos expresiones?
a) 1 && !!!(0 || 1) || !(0 || 1)
b) 1 & ~~~(0 | 1) | ~(0 | 1)
· 28 ¿Por qu´e si a es una variable entera a / 2 proporciona el mismo resultado que a >> 1?
¿Con qu´e operaci´on de bits puedes calcular a * 2? ¿Y a / 32? ¿Y a * 128?
· 29 ¿Qu´e hace este programa?
swap.c swap.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 unsigned char a, b;
6 printf ("Introduce el valor de a (entre 0 y 255): "); scanf ("%hhu",&a);
7 printf ("Introduce el valor de b (entre 0 y 255): "); scanf ("%hhu",&b);
8
9 a ^= b;
10 b ^= a;
11 a ^= b;
12
13 printf ("Valor de a: %hhun", a);
14 printf ("Valor de b: %hhun", b);
15
16 return 0;
17 }
(Nota: la forma en que hace lo que hace viene de un viejo truco de la programaci´on en
ensamblador, donde hay ricos juegos de instrucciones para la manipulaci´on de datos bit a bit.). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.13. Conversi´on impl´ıcita y expl´ıcita de tipos
El sistema de tipos escalares es m´as r´ıgido que el de Python, aunque m´as rico. Cuando se eval´ua
una expresi´on y el resultado se asigna a una variable, has de tener en cuenta el tipo de todos
los operandos y tambi´en el de la variable en la que se almacena.
Ilustraremos el comportamiento de C con fragmentos de programa que utilizan estas varia-
bles:
Introducci´on a la Programaci´on con C 41
1.13 Conversi´on impl´ıcita y expl´ıcita de tipos
¿5 > 3 > 2?
Recuerda que en Python pod´ıamos combinar operadores de comparaci´on para formar ex-
presiones como 5 > 3 > 2. Esa, en particular, se eval´ua a True, pues 5 es mayor que 3 y 3
es mayor que 2. C tambi´en acepta esa expresi´on, pero con un significado completamente
diferente basado en la asociatividad por la izquierda del operador >: en primer lugar eval´ua
la subexpresi´on 5 > 3, que proporciona el valor ((cierto)); pero como ((cierto)) es 1 (valor por
defecto) y 1 no es mayor que 2, el resultado de la evaluaci´on es 0, o sea, ((falso)).
¡Ojo con la interferencia entre ambos lenguajes! Problemas como ´este surgir´an con fre-
cuencia cuando aprendas nuevos lenguajes: construcciones que significan algo en el lenguaje
que conoces bien tienen un significado diferente en el nuevo.
char c;
int i;
float x;
Si asignas a un entero int el valor de un entero m´as corto, como un char, el entero corto
promociona a un entero int autom´aticamente. Es decir, es posible efectuar esta asignaci´on sin
riesgo alguno:
i = c;
Podemos igualmente asignar un entero int a un char. C se encarga de hacer la conversi´on de
tipos pertinente:
c = i;
Pero, ¿c´omo? ¡En un byte (lo que ocupa un char) no caben cuatro (los que ocupa un int)! C
toma los 8 bits menos significativos de i y los almacena en c, sin m´as. La conversi´on funciona
correctamente, es decir, preserva el valor, s´olo si el n´umero almacenado en i est´a comprendido
entre −128 y 127.
Observa este programa:
conversion delicada.c conversion delicada.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b;
6 char c, d;
7
8 a = 512;
9 b = 127;
10 c = a;
11 d = b;
12 printf ("%hhd %hhdn", c, d);
13
14 return 0;
15 }
Produce esta salida por pantalla:
0 127
¿Por qu´e el primer resultado es 0? El valor 512, almacenado en una variable de tipo int,
se representa con este patr´on de bits: 00000000000000000000001000000000. Sus 8 bits menos
significativos se almacenan en la variable c al ejecutar la asignaci´on c = a, es decir, c almacena
el patr´on de bits 00000000, que es el valor decimal 0.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 30 ¿Qu´e mostrar´a por pantalla este programa?
otra conversion delicada.c otra conversion delicada.c
1 #include <stdio.h>
42 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
2
3 int main(void)
4 {
5 int a, b;
6 char c, d;
7 unsigned char e, f;
8
9 a = 384;
10 b = 256;
11 c = a;
12 d = b;
13 e = a;
14 f = b;
15 printf ("%hhd %hhdn", c, d);
16 printf ("%hhu %hhun", e, f);
17
18 return 0;
19 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Si asignamos un entero a una variable flotante, el entero promociona a su valor equivalente
en coma flotante. Por ejemplo, esta asignaci´on almacena en x el valor 2.0 (no el entero 2).
x = 2;
Si asignamos un valor flotante a un entero, el flotante se convierte en su equivalente entero
(¡si lo hay!). Por ejemplo, la siguiente asignaci´on almacena el valor 2 en i (no el flotante 2.0).
i = 2.0;
Y esta otra asignaci´on almacena en i el valor 0:
i = 0.1;
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 31 ¿Qu´e valor se almacena en las variables i (de tipo int) y x (de tipo float) tras ejecutar
cada una de estas sentencias?
a) i = 2;
b) i = 1 / 2;
c) i = 2 / 4;
d) i = 2.0 / 4;
e) x = 2.0 / 4.0;
f) x = 2.0 / 4;
g) x = 2 / 4;
h) x = 1 / 2;
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Aunque C se encarga de efectuar impl´ıcitamente muchas de las conversiones de tipo, pue-
de que en ocasiones necesites indicar expl´ıcitamente una conversi´on de tipo. Para ello, debes
preceder el valor a convertir con el tipo de destino encerrado entre par´entesis. As´ı:
i = (int) 2.3;
En este ejemplo da igual poner (int) que no ponerlo: C hubiera hecho la conversi´on impl´ıci-
tamente. El t´ermino (int) es el operador de conversi´on a enteros de tipo int. Hay un operador
de conversi´on para cada tipo: (char), (unsigned int) (float), etc. . . Recuerda que el s´ımbolo
(tipo) es un operador unario conocido como operador de coerci´on o conversi´on de tipos.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 32 ¿Qu´e valor se almacena en las variables i (de tipo int) y x (de tipo float) tras ejecutar
estas sentencias?
a) i = (float) 2;
b) i = 1 / (float) 2;
c) i = (int) (2 / 4);
d) i = (int) 2. / (float) 4;
e) x = 2.0 / (int) 4.0;
f) x = (int) 2.0 / 4;
g) x = (int) (2.0 / 4);
h) x = 2 / (float) 4;
i) x = (float) (1 / 2);
j) x = 1 / (float) 2;
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Introducci´on a la Programaci´on con C 43
1.14 Las directivas y el preprocesador
1.14. Las directivas y el preprocesador
Las l´ıneas que empiezan con una palabra predecida por el car´acter # son especiales. Las palabras
que empiezan con # se denominan directivas. El compilador no llega a ver nunca las l´ıneas
que empiezan con una directiva. ¿Qu´e queremos decir exactamente con que no llega a verlas?
El compilador gcc es, en realidad, un programa que controla varias etapas en el proceso de
traducci´on de C a c´odigo de m´aquina. De momento, nos interesa considerar dos de ellas:
el preprocesador,
y el traductor de C a c´odigo de m´aquina (el compilador propiamente dicho).
programa.c Preprocesador Compilador programa
El preprocesador es un programa independiente, aunque es infrecuente invocarlo directamente.
El preprocesador del compilador gcc se llama cpp.
Las directivas son analizadas e interpretadas por el preprocesador. La directiva #include
seguida del nombre de un fichero (entre los caracteres < y >) hace que el preprocesador sustituya
la l´ınea en la que aparece por el contenido ´ıntegro del fichero (en ingl´es ((include)) significa
((incluye))). El compilador, pues, no llega a ver la directiva, sino el resultado de su sustituci´on.
Nosotros s´olo estudiaremos, de momento, dos directivas:
#define, que permite definir constantes,
e #include, que permite incluir el contenido de un fichero y que se usa para importar
funciones, variables, constantes, etc. de bibliotecas.
1.15. Constantes
1.15.1. Definidas con la directiva define
Una diferencia de C con respecto a Python es la posibilidad que tiene el primero de definir
constantes. Una constante es, en principio15
, una variable cuyo valor no puede ser modificado.
Las constantes se definen con la directiva #define. As´ı:
#define CONSTANTE valor
Cada l´ınea #define s´olo puede contener el valor de una constante.
Por ejemplo, podemos definir los valores aproximados de π y del n´umero e as´ı:
#define PI 3.1415926535897931159979634685442
#define E 2.7182818284590450907955982984276
Intentar asignar un valor a PI o a E en el programa produce un error que detecta el compi-
lador16
.
Observa que no hay operador de asignaci´on entre el nombre de la constante y su valor y
que la l´ınea no acaba con punto y coma17
. Es probable que cometas m´as de una vez el error de
escribir el operador de asignaci´on o el punto y coma.
No es obligatorio que el nombre de la constante se escriba en may´usculas, pero s´ı un convenio
ampliamente adoptado.
1.15.2. Definidas con el adjetivo const
C99 propone una forma alternativa de definir constantes mediante una nueva palabra reservada:
const. Puedes usar const delante del tipo de una variable inicializada en la declaraci´on para
indicar que su valor no se modificar´a nunca.
15Lo de ((en principio)) est´a justificado. No es cierto que las constantes de C sean variables. Lee el cuadro
titulado ((El preprocesador y las constantes)) para saber qu´e son exactamente.
16¿Has le´ıdo ya el cuadro ((El preprocesador y las constantes))?
17¿A qu´e esperas para leer el cuadro ((El preprocesador y las constantes))?
44 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
El preprocesador y las constantes
Como te dijimos antes, el compilador de C no compila directamente nuestros ficheros con
extensi´on ((.c)). Antes de compilarlos, son tratados por un programa al que se conoce
como preprocesador. El preprocesador (que en Unix suele ser el programa cpp, por ((C
preprocessor))) procesa las denominadas directivas (l´ıneas que empiezan con #). Cuando
el preprocesador encuentra la directiva #define, la elimina, pero recuerda la asociaci´on
establecida entre un identificador y un texto; cada vez que encuentra ese identificador en
el programa, lo sustituye por el texto. Un ejemplo ayudar´a a entender el porqu´e de algunos
errores misteriosos de C cuando se trabaja con constantes. Al compilar este programa:
preprocesar.c preprocesar.c
1 #define PI 3.14
2
3 int main(void)
4 {
5 int a = PI;
6 return 0;
7 }
el preprocesador lo transforma en este otro programa (sin modificar nuestro fichero). Puedes
comprobarlo invocando directamente al preprocesador:
$ cpp -P preprocesar.c
El resultado es esto:
1 int main(void)
2 {
3 int a = 3.14;
4 return 0;
5 }
Como puedes ver, una vez ((preprocesado)), no queda ninguna directiva en el programa y
la aparici´on del identificador PI ha sido sustituida por el texto 3.14. Un error t´ıpico es
confundir un #define con una declaraci´on normal de variables y, en consecuencia, poner
una asignaci´on entre el identificador y el valor:
1 #define PI = 3.14
2
3 int main(void)
4 {
5 int a = PI;
6 return 0;
7 }
El programa resultante es incorrecto. ¿Por qu´e? El compilador ve el siguiente programa tras
ser preprocesado:
1 int main(void)
2 {
3 int a = = 3.14;
4 return 0;
5 }
¡La tercera l´ınea del programa resultante no sigue la sintaxis del C!
constante.c constante.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 const float pi = 3.14;
6 float r, a;
7
Introducci´on a la Programaci´on con C 45
1.15 Constantes
8 printf ("Radio: ");
9 scanf ("%f", &r);
10
11 a = pi * r * r;
12
13 printf ("´Area: %fn", a);
14
15 return 0;
16 }
Pero la posibilidad de declarar constantes con const no nos libra de la directiva define,
pues no son de aplicaci´on en todo lugar donde conviene usar una constante. M´as adelante, al
estudiar la declaraci´on de vectores, nos referiremos nuevamente a esta cuesti´on.
1.15.3. Con tipos enumerados
Es frecuente definir una serie de constantes con valores consecutivos. Imagina una aplicaci´on
en la que escogemos una opci´on de un men´u como ´este:
1) Cargar registros
2) Guardar registros
3) A~nadir registro
4) Borrar registro
5) Modificar registro
6) Buscar registro
7) Finalizar
Cuando el usuario escoge una opci´on, la almacenamos en una variable (llam´emosla opcion) y
seleccionamos las sentencias a ejecutar con una serie de comparaciones como las que se muestran
aqu´ı esquem´aticamente18
:
if (opcion == 1) {
C´odigo para cargar registros
}
else if (opcion == 2) {
C´odigo para guardar registros
}
else if (opcion == 3) {
...
El c´odigo resulta un tanto ilegible porque no vemos la relaci´on entre los valores num´ericos y las
opciones de men´u. Es frecuente no usar los literales num´ericos y recurrir a constantes:
#define CARGAR 1
#define GUARDAR 2
#define ANYADIR 3
#define BORRAR 4
#define MODIFICAR 5
#define BUSCAR 6
#define FINALIZAR 7
...
if (opcion == CARGAR) {
C´odigo para cargar registros
}
else if (opcion == GUARDAR) {
C´odigo para guardar registros
}
else if (opcion == ANYADIR) {
...
18M´as adelante estudiaremos una estructura de selecci´on que no es if y que se usa normalmente para especificar
este tipo de acciones.
46 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
Puedes ahorrarte la retah´ıla de #defines con los denominados tipos enumerados. Un tipo
enumerado es un conjunto de valores ((con nombre)). F´ıjate en este ejemplo:
enum { Cargar=1, Guardar, Anyadir, Borrar, Modificar, Buscar, Finalizar };
...
if (opcion == Cargar) {
C´odigo para cargar registros
}
else if (opcion == Guardar) {
C´odigo para guardar registros
}
else if (opcion == Anyadir) {
...
La primera l´ınea define los valores Cargar, Guardar, . . . como una sucesi´on de valores
correlativos. La asignaci´on del valor 1 al primer elemento de la enumeraci´on hace que la sucesi´on
empiece en 1. Si no la hubi´esemos escrito, la sucesi´on empezar´ıa en 0.
Es habitual que los enum aparezcan al principio del programa, tras la aparici´on de los
#include y #define.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 33 ¿Qu´e valor tiene cada identificador de este tipo enumerado?
enum { Primera=’a’, Segunda, Tercera, Penultima=’y’, Ultima };
(No te hemos explicado qu´e hace la segunda asignaci´on. Comprueba que la explicaci´on que das
es correcta con un programa que muestre por pantalla el valor de cada identificador.)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Los tipos enumerados sirven para algo m´as que asignar valores a opciones de men´u. Es
posible definir identificadores con diferentes valores para series de elementos como los d´ıas de
la semana, los meses del a˜no, etc.
enum { Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo };
enum { Invierno, Primavera, Verano, Otonyo };
enum { Rojo, Verde, Azul };
1.16. Las bibliotecas (m´odulos) se importan con #include
En C, los m´odulos reciben el nombre de bibliotecas (o librer´ıas, como traducci´on fon´eticamente
similar del ingl´es library). La primera l´ınea de sumatorio.c es ´esta:
1 #include <stdio.h>
Con ella se indica que el programa hace uso de una biblioteca cuyas funciones, variables, tipos
de datos y constantes est´an declaradas en el fichero stdio.h, que es abreviatura de ((standard
input/output)) (entrada/salida est´andar). En particular, el programa sumatorio.c usa las fun-
ciones printf y scanf de stdio.h. Los ficheros con extensi´on ((.h)) se denominan ficheros cabecera
(la letra h es abreviatura de ((header)), que en ingl´es significa ((cabecera))).
A diferencia de Python, C no permite importar un subconjunto de las funciones propor-
cionadas por una biblioteca. Al hacer #include de una cabecera se importan todas sus fun-
ciones, tipos de datos, variables y constantes. Es como si en Python ejecutaras la sentencia
from m´odulo import *.
Normalmente no basta con incluir un fichero de cabecera con #include para poder compilar
un programa que utiliza bibliotecas. Es necesario, adem´as, compilar con opciones especiales.
Abundaremos sobre esta cuesti´on inmediatamente, al presentar la librer´ıa matem´atica.
1.16.1. La biblioteca matem´atica
Podemos trabajar con funciones matem´aticas incluyendo math.h en nuestros programas. La
tabla 1.1 relaciona algunas de las funciones que ofrece la biblioteca matem´atica.
Introducci´on a la Programaci´on con C 47
1.16 Las bibliotecas (m´odulos) se importan con #include
Funci´on C Funci´on matem´atica
sqrt(x) ra´ız cuadrada de x
sin(x) seno de x
cos(x) coseno de x
tan(x) tangente de x
asin(x) arcoseno de x
acos(x) arcocoseno de x
atan(x) arcotangente de x
exp(x) el n´umero e elevado a x
exp10(x) 10 elevado a x
log(x) logaritmo en base e de x
log10(x) logaritmo en base 10 de x
log2(x) logaritmo en base 2 de x
pow(x, y) x elevado a y
fabs(x) valor absoluto de x
round(x) redondeo al entero m´as pr´oximo a x
ceil(x) redondeo superior de x
floor(x) redondeo inferior de x
Tabla 1.1: Algunas funciones matem´aticas disponibles en la biblioteca math.h.
Todos los argumentos de las funciones de math.h son de tipo flotante.19
La biblioteca matem´atica tambi´en ofrece algunas constantes matem´aticas predefinidas. Te
relacionamos algunas en la tabla 1.2.
Constante Valor
M_E una aproximaci´on del n´umero e
M_PI una aproximaci´on del n´umero π
M_PI_2 una aproximaci´on de π/2
M_PI_4 una aproximaci´on de π/4
M_1_PI una aproximaci´on de 1/π
M_SQRT2 una aproximaci´on de
√
2
M_LOG2E una aproximaci´on de log2 e
M_LOG10E una aproximaci´on de log10 e
Tabla 1.2: Algunas constantes disponibles en la biblioteca math.h.
No basta con escribir #include <math.h> para poder usar las funciones matem´aticas: has
de compilar con la opci´on -lm:
$ gcc programa.c -lm -o programa
¿Por qu´e? Cuando haces #include, el preprocesador introduce un fragmento de texto que
dice qu´e funciones pasan a estar accesibles, pero ese texto no dice qu´e hace cada funci´on y c´omo
lo hace (con qu´e instrucciones concretas). Si compilas sin -lm, el compilador se ((quejar´a)):
$ gcc programa.c -o programa
/tmp/ccm1nE0j.o: In function ‘main’:
/tmp/ccm1nE0j.o(.text+0x19): undefined reference to ‘sqrt’
collect2: ld returned 1 exit status
El mensaje advierte de que hay una ((referencia indefinida a sqrt)). En realidad no se est´a
((quejando)) el compilador, sino otro programa del que a´un no te hemos dicho nada: el enlazador
(en ingl´es, ((linker))). El enlazador es un programa que detecta en un programa las llamadas a
funci´on no definidas en un programa C y localiza la definici´on de las funciones (ya compiladas) en
bibliotecas. El fichero math.h que inclu´ımos con #define contiene la cabecera de las funciones
19Lo cierto es que son de tipo double (v´ease el ap´endice A), pero no hay problema si las usas con valores y
variables de tipo float, ya que hay conversi´on autom´atica de tipos.
48 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
matem´aticas, pero no su cuerpo. El cuerpo de dichas funciones, ya compilado (es decir, en
c´odigo de m´aquina), reside en otro fichero: /usr/lib/libm.a. ¿Para qu´e vale el fichero math.h
si no tiene el cuerpo de las funciones? Para que el compilador compruebe que estamos usando
correctamente las funciones (que suministramos el n´umero de argumentos adecuado, que su
tipo es el que debe ser, etc.). Una vez que se comprueba que el programa es correcto, se procede
a generar el c´odigo de m´aquina, y ah´ı es necesario ((pegar)) (((enlazar))) el c´odigo de m´aquina de
las funciones matem´aticas que hemos utilizado. El cuerpo ya compilado de sqrt, por ejemplo,
se encuentra en /usr/lib/libm.a (libm es abreviatura de ((math library))). El enlazador es el
programa que ((enlaza)) el c´odigo de m´aquina de nuestro programa con el c´odigo de m´aquina de
las bibliotecas que usamos. Con la opci´on -lm le indicamos al enlazador que debe resolver las
referencias indefinidas a funciones matem´aticas utilizando /usr/lib/libm.a.
La opci´on -lm evita tener que escribir /usr/lib/libm.a al final. Estas dos invocaciones del
compilador son equivalentes:
$ gcc programa.c -o programa -lm
$ gcc programa.c -o programa /usr/lib/libm.a
El proceso completo de compilaci´on cuando enlazamos con /usr/lib/libm.a puede repre-
sentarse gr´aficamente as´ı:
programa.c Preprocesador Compilador Enlazador programa
/usr/lib/libm.a
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 34 Dise˜na un programa C que solicite la longitud de los tres lados de un tri´angulo (a, b y c)
y muestre por pantalla su per´ımetro y su ´area ( s(s − a)(s − b)(s − c), donde s = (a+b+c)/2.).
Compila y ejecuta el programa.
· 35 Dise˜na un programa C que solicite el radio r de una circunferencia y muestre por
pantalla su per´ımetro (2πr) y su ´area (πr2
). Utiliza la aproximaci´on a π predefinida en la
biblioteca matem´atica.
Compila y ejecuta el programa.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.17. Estructuras de control
Las estructuras de control de C son parecidas a las de Python. Bueno, hay alguna m´as y
todas siguen unas reglas sint´acticas diferentes. Empecemos estudiando las estructuras de control
condicionales.
1.17.1. Estructuras de control condicionales
La sentencia de selecci´on if
La estructura de control condicional fundamental es el if. En C se escribe as´ı:
if (condici´on) {
sentencias
}
Los par´entesis que encierran a la condici´on son obligatorios. Como en Python no lo son, es f´acil
que te equivoques por no ponerlos. Si el bloque de sentencias consta de una sola sentencia, no
es necesario encerrarla entre llaves:
if (condici´on)
sentencia;
Introducci´on a la Programaci´on con C 49
1.17 Estructuras de control
La sentencia de selecci´on if-else
Hay una forma if-else, como en Python:
if (condici´on) {
sentencias_si
}
else {
sentencias_no
}
Si uno de los bloques s´olo tiene una sentencia, generalmente puedes eliminar las llaves:
if (condici´on)
sentencia_si;
else {
sentencias_no
}
if (condici´on) {
sentencias_si
}
else
sentencia_no;
if (condici´on)
sentencia_si;
else
sentencia_no;
Ojo: la indentaci´on no significa nada para el compilador. La ponemos ´unicamente para
facilitar la lectura. Pero si la indentaci´on no significa nada nos enfrentamos a un problema de
ambig¨uedad con los if anidados:
if (condici´on)
if (otra_condici´on) {
sentencias_si
}
else { //
???
???
sentencias_no
}
¿A cu´al de los dos if pertenece el else? ¿Har´a el compilador de C una interpretaci´on como la
que sugiere la indentaci´on en el ´ultimo fragmento o como la que sugiere este otro?:
if (condici´on)
if (otra_condici´on) {
sentencias_si
}
else { //
???
???
sentencias_no
}
C rompe la ambig¨uedad trabajando con esta sencilla regla: el else pertenece al if ((libre))
m´as cercano. Si quisi´eramos expresar la primera estructura, deber´ıamos a˜nadir llaves para
determinar completamente qu´e bloque est´a dentro de qu´e otro:
if (condici´on) {
if (otra_condici´on) {
sentencias_si
}
}
else {
sentencias_no
}
El if externo contiene una sola sentencia (otro if) y, por tanto, las llaves son redundantes;
pero hacen evidente que el else va asociado a la condici´on exterior.
50 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
No hay sentencia elif: la combinaci´on else if
C no tiene una estructura elif como la de Python, pero tampoco la necesita. Puedes usar else if
donde hubieras puesto un elif en Python:
if (condici´on) {
sentencias_si
}
else if (condici´on2) {
sentencias_si2
}
else if (condici´on3) {
sentencias_si3
}
else {
sentencias_no
}
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 36 Dise˜na un programa C que pida por teclado un n´umero entero y diga si es par o impar.
· 37 Dise˜na un programa que lea dos n´umeros enteros y muestre por pantalla, de estos tres
mensajes, el que convenga:
((El segundo es el cuadrado exacto del primero.)),
((El segundo es menor que el cuadrado del primero.)),
((El segundo es mayor que el cuadrado del primero.)).
· 38 Tambi´en en C es problem´atica la divisi´on por 0. Haz un programa C que resuelva la
ecuaci´on ax+b = 0 solicitando por teclado el valor de a y b (ambos de tipo float). El programa
detectar´a si la ecuaci´on no tiene soluci´on o si tiene infinitas soluciones y, en cualquiera de los
dos casos, mostrar´a el pertinente aviso.
· 39 Dise˜na un programa que solucione ecuaciones de segundo grado. El programa detectar´a
y tratar´a por separado las siguientes situaciones:
la ecuaci´on tiene dos soluciones reales;
la ecuaci´on tiene una ´unica soluci´on real;
la ecuaci´on no tiene soluci´on real;
la ecuaci´on tiene infinitas soluciones.
· 40 Realiza un programa que proporcione el desglose en billetes y monedas de una cantidad
exacta de euros. Hay billetes de 500, 200, 100, 50, 20, 10 y 5 euros y monedas de 1 y 2 euros.
Por ejemplo, si deseamos conocer el desglose de 434 euros, el programa mostrar´a por pantalla
el siguiente resultado:
2 billetes de 200 euros.
1 billete de 20 euros.
1 billete de 10 euros.
2 monedas de 2 euros.
Observa que la palabra ((billete)) (y ((moneda))) concuerda en n´umero con la cantidad de
billetes (o monedas) y que si no hay piezas de un determinado tipo (en el ejemplo, de 1 euro),
no muestra el mensaje correspondiente.
· 41 Dise˜na un programa C que lea un car´acter cualquiera desde el teclado, y muestre el
mensaje ((Es una MAY´USCULA.)) cuando el car´acter sea una letra may´uscula y el mensaje ((Es
una MIN´USCULA.)) cuando sea una min´uscula. En cualquier otro caso, no mostrar´a mensaje
alguno. (Considera ´unicamente letras del alfabeto ingl´es.)
Introducci´on a la Programaci´on con C 51
1.17 Estructuras de control
· 42 Dise˜na un programa que lea cinco n´umeros enteros por teclado y determine cu´al de los
cuatro ´ultimos n´umeros es m´as cercano al primero.
(Por ejemplo, si el usuario introduce los n´umeros 2, 6, 4, 1 y 10, el programa responder´a
que el n´umero m´as cercano al 2 es el 1.)
· 43 Dise˜na un programa que, dado un n´umero entero, determine si ´este es el doble de un
n´umero impar.
(Ejemplo: 14 es el doble de 7, que es impar.)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
La sentencia de selecci´on switch
Hay una estructura condicional que no existe en Python: la estructura de selecci´on m´ultiple. Esta
estructura permite seleccionar un bloque de sentencias en funci´on del valor de una expresi´on
(t´ıpicamente una variable).
1 switch (expresi´on) {
2 case valor1:
3 sentencias
4 break;
5 case valor2:
6 sentencias
7 break;
8 ...
9 default:
10 sentencias
11 break;
12 }
El fragmento etiquetado con default es opcional.
Para ilustrar el uso de switch, nada mejor que un programa que muestra algo por pantalla
en funci´on de la opci´on seleccionada de un men´u:
menu.c menu.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int opcion;
6
7 printf ("1) Saludan");
8 printf ("2) Desp´ıdeten");
9 scanf ("%d", &opcion);
10 switch (opcion) {
11 case 1:
12 printf ("Holan");
13 break;
14 case 2:
15 printf ("Adi´osn");
16 break;
17 default:
18 printf ("Opci´on no v´alidan");
19 break;
20 }
21 return 0;
22 }
Aunque resulta algo m´as elegante esta otra versi´on, que hace uso de tipos enumerados:
menu 1.c menu.c
1 #include <stdio.h>
2
3 enum { Saludar=1, Despedirse };
4
52 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
5 int main(void)
6 {
7 int opcion;
8
9 printf ("1) Saludan");
10 printf ("2) Desp´ıdeten");
11 scanf ("%d", &opcion);
12 switch (opcion) {
13 case Saludar :
14 printf ("Holan");
15 break;
16 case Despedirse :
17 printf ("Adi´osn");
18 break;
19 default:
20 printf ("Opci´on no v´alidan");
21 break;
22 }
23 return 0;
24 }
Un error t´ıpico al usar la estructura switch es olvidar el break que hay al final de cada
opci´on. Este programa, por ejemplo, presenta un comportamiento curioso:
menu2.c E menu2.c E
1 #include <stdio.h>
2
3 enum { Saludar=1, Despedirse };
4
5 int main(void)
6 {
7 int opcion;
8
9 printf ("1) Saludan");
10 printf ("2) Desp´ıdeten");
11 scanf ("%d", &opcion);
12 switch (opcion) {
13 case Saludar:
14 printf ("Holan");
15 case Despedirse:
16 printf ("Adi´osn");
17 default:
18 printf ("Opci´on no v´alidan");
19 }
20 return 0;
21 }
Si seleccionas la opci´on 1, no sale un ´unico mensaje por pantalla, ¡salen tres: Hola, Adi´os
y Opci´on no v´alida! Y si seleccionas la opci´on 2, ¡salen dos mensajes: Adi´os y Opci´on no
v´alida! Si no hay break, el flujo de control que entra en un case ejecuta las acciones asociadas
al siguiente case, y as´ı hasta encontrar un break o salir del switch por la ´ultima de sus l´ıneas.
El compilador de C no se˜nala la ausencia de break como un error porque, de hecho, no lo
es. Hay casos en los que puedes explotar a tu favor este curioso comportamiento del switch:
menu3.c menu3.c
1 #include <stdio.h>
2
3 enum { Saludar=1, Despedirse, Hola, Adios };
4
5 int main(void)
6 {
7 int opcion;
8
9 printf ("1) Saludan");
Introducci´on a la Programaci´on con C 53
1.17 Estructuras de control
10 printf ("2) Desp´ıdeten");
11 printf ("3) Di holan");
12 printf ("4) Di adi´osn");
13 scanf ("%d", &opcion);
14 switch (opcion) {
15 case Saludar:
16 case Hola:
17 printf ("Holan");
18 break;
19 case Despedirse:
20 case Adios:
21 printf ("Adi´osn");
22 break;
23 default:
24 printf ("Opci´on no v´alidan");
25 break;
26 }
27 return 0;
28 }
¿Ves por qu´e?
1.17.2. Estructuras de control iterativas
El bucle while
El bucle while de Python se traduce casi directamente a C:
while (condici´on) {
sentencias
}
Nuevamente, los par´entesis son obligatorios y las llaves pueden suprimirse si el bloque contiene
una sola sentencia.
Veamos un ejemplo de uso: un programa que calcula xn
para x y n enteros:
potencia.c potencia.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int x, n, i, r;
6
7 printf ("x: "); scanf ("%d", &x);
8 printf ("n: "); scanf ("%d", &n);
9 r = 1;
10 i = 0;
11 while (i < n) {
12 r *= x;
13 i++;
14 }
15 printf ("%d**%d = %dn", x, n, r);
16
17 return 0;
18 }
El bucle do-while
Hay un bucle iterativo que Python no tiene: el do-while:
do {
sentencias
} while (condici´on);
54 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
El bucle do-while eval´ua la condici´on tras cada ejecuci´on de su bloque, as´ı que es seguro
que ´este se ejecuta al menos una vez. Podr´ıamos reescribir sumatorio.c para usar un bucle
do-while:
sumatorio 2.c sumatorio.c
1 #include <stdio.h>
2 #include <math.h>
3
4 int main(void)
5 {
6 int a, b, i;
7 float s;
8
9 /* Pedir l´ımites inferior y superior. */
10 do {
11 printf ("L´ımite inferior:"); scanf ("%d", &a);
12 if (a < 0) printf ("No puede ser negativon");
13 } while (a < 0);
14
15 do {
16 printf ("L´ımite superior:"); scanf ("%d", &b);
17 if (b < a) printf ("No puede ser menor que %dn", a);
18 } while (b < a);
19
20 /* Calcular el sumatorio de la ra´ız cuadrada de i para i entre a y b. */
21 s = 0.0;
22 for (i = a; i <= b; i++) s += sqrt(i);
23
24 /* Mostrar el resultado. */
25 printf ("Sumatorio de ra´ıces de %d a %d: %fn", a, b, s);
26
27 return 0;
28 }
Los bucles do-while no a˜naden potencia al lenguaje, pero s´ı lo dotan de mayor expresividad.
Cualquier cosa que puedas hacer con bucles do-while, puedes hacerla tambi´en con s´olo bucles
while y la ayuda de alguna sentencia condicional if, pero probablemente requerir´an mayor
esfuerzo por tu parte.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 44 Escribe un programa que muestre un men´u en pantalla con dos opciones: ((saludar)) y
((salir)). El programa pedir´a al usuario una opci´on y, si es v´alida, ejecutar´a su acci´on asociada.
Mientras no se seleccione la opci´on ((salir)), el men´u reaparecer´a y se solicitar´a nuevamente una
opci´on. Implementa el programa haciendo uso ´unicamente de bucles do-while.
· 45 Haz un programa que pida un n´umero entero de teclado distinto de 1. A continuaci´on,
el programa generar´a una secuencia de n´umeros enteros cuyo primer n´umero es el que hemos
le´ıdo y que sigue estas reglas:
si el ´ultimo n´umero es par, el siguiente resulta de dividir a ´este por la mitad;
si el ´ultimo n´umero es impar, el siguiente resulta de multiplicarlo por 3 y a˜nadirle 1.
Todos los n´umeros se ir´an mostrando por pantalla conforme se vayan generando. El proceso se
repetir´a hasta que el n´umero generado sea igual a 1. Utiliza un bucle do-while.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
El bucle for
El bucle for de Python existe en C, pero con importantes diferencias.
for (inicializaci´on; condici´on; incremento) {
sentencias
}
Introducci´on a la Programaci´on con C 55
1.17 Estructuras de control
Comparaciones y asignaciones
Un error frecuente es sustituir el operador de comparaci´on de igualdad por el de asignaci´on
en una estructura if o while. Analiza este par de sentencias:
a = 0
if (a = 0) { // Lo que escribi´o...
?
bien o mal?
...
}
Parece que la condici´on del if se eval´ua a cierto, pero no es as´ı: la ((comparaci´on)) es, en
realidad, una asignaci´on. El resultado es que a recibe el valor 0 y que ese 0, devuelto por el
operador de asignaci´on, se considera la representaci´on del valor ((falso)). Lo correcto hubiera
sido:
a = 0
if (a == 0) { // Lo que quer´ıa escribir.
...
}
Aunque esta construcci´on es perfectamente v´alida, provoca la emisi´on de un mensaje de
error en muchos compiladores, pues suele ser fruto de un error.
Los programadores m´as disciplinados evitan cometer este error escribiendo siempre la
variable en la parte derecha:
a = 0
if (0 == a) { // Correcto.
...
}
De ese modo, si se confunden y usan = en lugar de ==, se habr´a escrito una expresi´on
incorrecta y el compilador detendr´a el proceso de traducci´on a c´odigo de m´aquina:
a = 0
if (0 = a) { // Mal: error detectable por el compilador.
...
}
Los par´entesis de la primera l´ınea son obligatorios. F´ıjate, adem´as, en que los tres elementos
entre par´entesis se separan con puntos y comas.
El bucle for presenta tres componentes. Es equivalente a este fragmento de c´odigo:
inicializaci´on;
while (condici´on) {
sentencias
incremento;
}
Una forma habitual de utilizar el bucle for es la que se muestra en este ejemplo, que imprime
por pantalla los n´umeros del 0 al 9 y en el que suponemos que i es de tipo int:
for (i = 0; i < 10; i++) {
printf ("%dn", i);
}
Es equivalente, como dec´ıamos, a este otro fragmento de programa:
i = 0;
while (i < 10) {
printf ("%dn", i);
i++;
}
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 46 Implementa el programa de c´alculo de xn
(para x y n entero) con un bucle for.
56 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
· 47 Implementa un programa que dado un n´umero de tipo int, le´ıdo por teclado, se asegure
de que s´olo contiene ceros y unos y muestre su valor en pantalla si lo interpretamos como un
n´umero binario. Si el usuario introduce, por ejemplo, el n´umero 1101, el programa mostrar´a el
valor 13. Caso de que el usuario introduzca un n´umero formado por n´umeros de valor diferente,
indica al usuario que no puedes proporcionar el valor de su interpretaci´on como n´umero binario.
· 48 Haz un programa que solicite un n´umero entero y muestre su factorial. Utiliza un entero
de tipo long long para el resultado. Debes usar un bucle for.
· 49 El n´umero de combinaciones de n elementos tomados de m en m es:
Cm
n =
n
m
=
n!
(n − m)! m!
.
Dise˜na un programa que pida el valor de n y m y calcule Cm
n . (Ten en cuenta que n ha de ser
mayor o igual que m.)
(Puedes comprobar la validez de tu programa introduciendo los valores n = 15 y m = 10:
el resultado es 3003.)
· 50 ¿Qu´e muestra por pantalla este programa?
desplazamientos.c desplazamientos.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a = 127, b = 1024, c, i;
6
7 c = a ^ b;
8
9 printf ("%dn", c);
10
11 a = 2147483647;
12 for (i = 0; i < 8*sizeof(a); i++) {
13 printf ("%d", ((c & a) != 0) ? 1 : 0);
14 a >>= 1;
15 }
16 printf ("n");
17
18 a = 1;
19 for (i = 0; i < 8*sizeof(a); i++) {
20 if ((c & a) != 0) c >>= 1;
21 else c <<= 1;
22 a <<= 1;
23 }
24
25 a = 2147483647;
26 for (i = 0; i < 8*sizeof(a); i++) {
27 printf ("%d", ((c & a) != 0) ? 1 : 0);
28 a >>= 1;
29 }
30 printf ("n");
31 return 0;
32 }
· 51 Cuando no era corriente el uso de terminales gr´aficos de alta resoluci´on era com´un
representar gr´aficas de funciones con el terminal de caracteres. Por ejemplo, un periodo de la
funci´on seno tiene este aspecto al representarse en un terminal de caracteres (cada punto es un
asterisco):
*
*
*
*
*
*
*
*
*
*
Introducci´on a la Programaci´on con C 57
1.17 Estructuras de control
*
*
*
*
*
*
*
*
*
*
*
*
*
*
Haz un programa C que muestre la funci´on seno utilizando un bucle que recorre el periodo 2π
en 24 pasos (es decir, represent´andolo con 24 l´ıneas).
· 52 Modifica el programa para que muestre las funciones seno (con asteriscos) y coseno (con
sumas) simult´aneamente.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Variables de bucle de usar y tirar
C99 ha copiado una buena idea de C++: permitir que las variables de bucle se definan all´ı
donde se usan y dejen de existir cuando el bucle termina. F´ıjate en este programa:
for con variable.c for con variable.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a = 1;
6
7 for (int i = 0; i < 32; i++) {
8 printf ("2**%2d = %10un", i, a);
9 a <<= 1;
10 }
11
12 return 0;
13 }
La variable i, el ´ındice del bucle, se declara en la mism´ısima zona de inicializaci´on del bucle.
La variable i s´olo existe en el ´ambito del bucle, que es donde se usa.
Hacer un bucle que recorra, por ejemplo, los n´umeros pares entre 0 y 10 es sencillo: basta
sustituir el modo en que se incrementa la variable ´ındice:
for (i = 0; i < 10; i = i + 2) {
printf ("%dn", i);
}
aunque la forma habitual de expresar el incremento de i es esta otra:
for (i = 0; i < 10; i += 2) {
printf ("%dn", i);
}
Un bucle que vaya de 10 a 1 en orden inverso presenta este aspecto:
for (i = 10; i > 0; i--) {
printf ("%dn", i);
}
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 53 Dise˜na un programa C que muestre el valor de 2n
para todo n entre 0 y un valor entero
proporcionado por teclado.
58 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
· 54 Haz un programa que pida al usuario una cantidad de euros, una tasa de inter´es y
un n´umero de a˜nos y muestre por pantalla en cu´anto se habr´a convertido el capital inicial
transcurridos esos a˜nos si cada a˜no se aplica la tasa de inter´es introducida.
Recuerda que un capital C a un inter´es del x por cien durante n a˜nos se convierte en
C · (1 + x/100)n
.
(Prueba tu programa sabiendo que 10 000 euros al 4.5% de inter´es anual se convierten en
24 117.14 euros al cabo de 20 a˜nos.)
· 55 Un vector en un espacio tridimensional es una tripleta de valores reales (x, y, z). Desea-
mos confeccionar un programa que permita operar con dos vectores. El usuario ver´a en pantalla
un men´u con las siguientes opciones:
1) Introducir el primer vector
2) Introducir el segundo vector
3) Calcular la suma
4) Calcular la diferencia
5) Calcular el producto vectorial
6) Calcular el producto escalar
7) Calcular el ´angulo (en grados) entre ellos
8) Calcular la longitud
9) Finalizar
Tras la ejecuci´on de cada una de las acciones del men´u ´este reaparecer´a en pantalla, a menos
que la opci´on escogida sea la n´umero 9. Si el usuario escoge una opci´on diferente, el programa
advertir´a al usuario de su error y el men´u reaparecer´a.
Las opciones 4 y 5 pueden proporcionar resultados distintos en funci´on del orden de los
operandos, as´ı que, si se escoge cualquiera de ellas, aparecer´a un nuevo men´u que permita
seleccionar el orden de los operandos. Por ejemplo, la opci´on 4 mostrar´a el siguiente men´u:
1) Primer vector menos segundo vector
2) Segundo vector menos primer vector
Nuevamente, si el usuario se equivoca, se le advertir´a del error y se le permitir´a corregirlo.
La opci´on 8 del men´u principal conducir´a tambi´en a un submen´u para que el usuario decida
sobre qu´e vector se aplica el c´alculo de longitud.
Puede que necesites que te refresquemos la memoria sobre los c´alculos a realizar. Quiz´a la
siguiente tabla te sea de ayuda:
Operaci´on C´alculo
Suma: (x1, y1, z1) + (x2, y2, z2) (x1 + x2, y1 + y2, z1 + z2)
Diferencia: (x1, y1, z1) − (x2, y2, z2) (x1 − x2, y1 − y2, z1 − z2)
Producto escalar: (x1, y1, z1) · (x2, y2, z2) x1x2 + y1y2 + z1z2
Producto vectorial: (x1, y1, z1) × (x2, y2, z2) (y1z2 − z1y2, z1x2 − x1z2, x1y2 − y1x2)
´Angulo entre (x1, y1, z1) y (x2, y2, z2)
180
π
· arccos
x1x2 + y1y2 + z1z2
x2
1 + y2
1 + z2
1 x2
2 + y2
2 + z2
2
Longitud de (x, y, z) x2 + y2 + z2
Ten en cuenta que tu programa debe contemplar toda posible situaci´on excepcional: divi-
siones por cero, ra´ıces con argumento negativo, etc..
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.17.3. Sentencias para alterar el flujo iterativo
La sentencia break tambi´en est´a disponible en C. De hecho, ya hemos visto una aplicaci´on suya
en la estructura de control switch. Con ella puedes, adem´as, abortar al instante la ejecuci´on
de un bucle cualquiera (while, do-while o for).
Otra sentencia de C que puede resultar ´util es continue. Esta sentencia finaliza la iteraci´on
actual, pero no aborta la ejecuci´on del bucle.
Introducci´on a la Programaci´on con C 59
1.17 Estructuras de control
Por ejemplo, cuando en un bucle while se ejecuta continue, la siguiente sentencia a ejecutar
es la condici´on del bucle; si ´esta se cumple, se ejecutar´a una nueva iteraci´on del bucle.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 56 ¿Qu´e muestra por pantalla este programa?
continue.c continue.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i;
6
7 i = 0;
8 while (i < 10) {
9 if (i % 2 == 0) {
10 i++;
11 continue;
12 }
13 printf ("%dn", i);
14 i++;
15 }
16
17 for (i = 0; i < 10; i++) {
18 if (i % 2 != 0)
19 continue;
20 printf ("%dn", i);
21 }
22 return 0;
23 }
· 57 Traduce a C este programa Python.
1 car = raw_input(’Dame un car´acter: ’)
2 if "a" <= car.lower() <= "z" or car == " ":
3 print "Este car´acter es v´alido en un identificador en Python."
4 else:
5 if not (car < "0" or "9" < car):
6 print "Un d´ıgito es v´alido en un identificador en Python,",
7 print "siempre que no sea el primer car´acter."
8 else:
9 print "Car´acter no v´alido para formar un identificador en Python."
· 58 Traduce a C este programa Python.
1 from math import pi
2 radio = float(raw_input(’Dame el radio de un c´ırculo: ’))
3 opcion = ’’
4 while opcion != ’a’ and opcion != ’b’ and opcion != ’c’:
5 print ’Escoge una opci´on: ’
6 print ’a) Calcular el di´ametro.’
7 print ’b) Calcular el per´ımetro.’
8 print ’c) Calcular el ´area.’
9 opcion = raw_input(’Teclea a, b o c y pulsa el retorno de carro: ’)
10 if opcion == ’a’:
11 diametro = 2 * radio
12 print ’El di´ametro es’, diametro
13 elif opcion == ’b’:
14 perimetro = 2 * pi * radio
15 print ’El per´ımetro es’, perimetro
16 elif opcion == ’c’:
17 area = pi * radio ** 2
18 print ’El ´area es’, area
19 else:
20 print ’S´olo hay tres opciones: a, b o c. T´u has tecleado’, opcion
60 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C
· 59 Traduce a C este programa Python.
1 anyo = int(raw_input(’Dame un a~no: ’))
2 if anyo % 4 == 0 and (anyo % 100 != 0 or anyo % 400 == 0):
3 print ’El a~no’, anyo, ’es bisiesto.’
4 else:
5 print ’El a~no’, anyo, ’no es bisiesto.’
· 60 Traduce a C este programa Python.
1 limite = int(raw_input(’Dame un n´umero: ’))
2
3 for num in range(1, limite+1):
4 creo_que_es_primo = 1
5 for divisor in range(2, num):
6 if num % divisor == 0:
7 creo_que_es_primo = 0
8 break
9 if creo_que_es_primo == 1:
10 print num
· 61 Escribe un programa que solicite dos enteros n y m asegur´andose de que m sea mayor
o igual que n. A continuaci´on, muestra por pantalla el valor de
m
i=n 1/i.
· 62 Escribe un programa que solicite un n´umero entero y muestre todos los n´umeros primos
entre 1 y dicho n´umero.
· 63 Haz un programa que calcule el m´aximo com´un divisor (mcd) de dos enteros positivos.
El mcd es el n´umero m´as grande que divide exactamente a ambos n´umeros.
· 64 Haz un programa que calcule el m´aximo com´un divisor (mcd) de tres enteros positivos.
· 65 Haz un programa que vaya leyendo n´umeros y mostr´andolos por pantalla hasta que el
usuario introduzca un n´umero negativo. En ese momento, el programa acabar´a mostrando un
mensaje de despedida.
· 66 Haz un programa que vaya leyendo n´umeros hasta que el usuario introduzca un n´umero
negativo. En ese momento, el programa mostrar´a por pantalla el n´umero mayor de cuantos ha
visto.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Introducci´on a la Programaci´on con C 61
1.17 Estructuras de control
62 Introducci´on a la Programaci´on con C
Cap´ıtulo 2
Estructuras de datos en C: vectores
est´aticos y registros
—Me llamo Alicia, Majestad —dijo Alicia con mucha educaci´on; pero a˜nadi´o para sus
adentros: ((¡Vaya!, en realidad no son m´as que un mazo de cartas. ¡No tengo por qu´e
tenerles miedo!)).
Lewis Carroll, Alicia en el Pa´ıs de las Maravillas.
En este cap´ıtulo vamos a estudiar algunas estructuras que agrupan varios datos, pero cuyo
tama˜no resulta conocido al compilar el programa y no sufre modificaci´on alguna durante su
ejecuci´on. Empezaremos estudiando los vectores, estructuras que se pueden asimilar a las listas
Python. En C, las cadenas son un tipo particular de vector. Manejar cadenas en C resulta
m´as complejo y delicado que manejarlas en Python. Como contrapartida, es m´as f´acil definir
en C vectores multidimensionales (como las matrices) que en Python. En este cap´ıtulo nos
ocuparemos tambi´en de ellos. Estudiaremos adem´as los registros en C, que permiten definir
nuevos tipos como agrupaciones de datos de tipos no necesariamente id´enticos. Los registros de
C son conceptualmente id´enticos a los que estudiamos en Python.
2.1. Vectores est´aticos
Un vector (en ingl´es, ((array))) es una secuencia de valores a los que podemos acceder mediante
´ındices que indican sus respectivas posiciones. Los vectores pueden asimilarse a las listas Python,
pero con una limitaci´on fundamental: todos los elementos del vector han de tener el mismo tipo.
Podemos definir vectores de enteros, vectores de flotantes, etc., pero no podemos definir vectores
que, por ejemplo, contengan a la vez enteros y flotantes. El tipo de los elementos de un vector
se indica en la declaraci´on del vector.
C nos permite trabajar con vectores est´aticos y din´amicos. En este cap´ıtulo nos ocupamos
´unicamente de los denominados vectores est´aticos, que son aquellos que tienen tama˜no fijo
y conocido en tiempo de compilaci´on. Es decir, el n´umero de elementos del vector no puede
depender de datos que suministra el usuario: se debe hacer expl´ıcito mediante una expresi´on
que podamos evaluar examinando ´unicamente el texto del programa.
2.1.1. Declaraci´on de vectores
Un vector a de 10 enteros de tipo int se declara as´ı:
int a[10];
El vector a comprende los elementos a[0], a[1], a[2], . . . , a[9], todos de tipo int. Al igual
que con las listas Python, los ´ındices de los vectores C empiezan en cero.
En una misma l´ınea puedes declarar m´as de un vector, siempre que todos compartan el
mismo tipo de datos para sus componentes. Por ejemplo, en esta l´ınea se declaran dos vectores
de float, uno con 20 componentes y otro con 100:
Introducci´on a la Programaci´on con C 63
2.1 Vectores est´aticos
Sin cortes
Los vectores C son mucho m´as limitados que las listas Python. A los problemas relacionados
con el tama˜no fijo de los vectores o la homogeneidad en el tipo de sus elementos se une
una incomodidad derivada de la falta de operadores a los que nos hemos acostumbrado
como programadores Python. El operador de corte, por ejemplo, no existe en C. Cuando
en Python dese´abamos extraer una copia de los elementos entre i y j de un vector a
escrib´ıamos a[i:j+1]. En C no hay operador de corte. . . ni operador de concatenaci´on o
repetici´on, ni sentencias de borrado de elementos, ni se entienden como accesos desde el
final los´ındices negativos, ni hay operador de pertenencia, etc. Echaremos de menos muchas
de las facilidades propias de Python.
float a[20], b[100];
Tambi´en es posible mezclar declaraciones de vectores y escalares en una misma l´ınea. En
este ejemplo se declaran las variables a y c como vectores de 80 caracteres y la variable b como
escalar de tipo car´acter:
char a[80], b, c[80];
Se considera mal estilo declarar la talla de los vectores con literales de entero. Es preferible
utilizar alg´un identificador para la talla, pero teniendo en cuenta que ´este debe corresponder a
una constante:
#define TALLA 80
...
char a[TALLA];
Esta otra declaraci´on es incorrecta, pues usa una variable para definir la talla del vector1
:
int talla = 80;
...
char a[talla]; //
!
No siempre es v´alido!
Puede que consideres v´alida esta otra declaraci´on que prescinde de constantes definidas con
define y usa constantes declaradas con const, pero no es as´ı:
const int talla = 80;
...
char a[talla]; //
!
No siempre es v´alido!
Una variable const es una variable en toda regla, aunque de ((s´olo lectura)).
2.1.2. Inicializaci´on de los vectores
Una vez creado un vector, sus elementos presentan valores arbitrarios. Es un error suponer que
los valores del vector son nulos tras su creaci´on. Si no lo crees, f´ıjate en este programa:
sin inicializar.c sin inicializar.c
1 #include <stdio.h>
2
3 #define TALLA 5
4
5 int main(void)
6 {
7 int i, a[TALLA];
8
9 for (i = 0; i < TALLA; i++)
10 printf ("%dn", a[i]);
11 return 0;
12 }
1Como siempre, hay excepciones: C99 permite declarar la talla de un vector con una expresi´on cuyo valor
s´olo se conoce en tiempo de ejecuci´on, pero s´olo si el vector es una variable local a una funci´on. Para evitar
confusiones, no haremos uso de esa caracter´ıstica en este cap´ıtulo y lo consideraremos incorrecto.
64 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
Observa que el acceso a elementos del vector sigue la misma notaci´on de Python: usamos
el identificador del vector seguido del ´ındice encerrado entre corchetes. En una ejecuci´on del
programa obtuvimos este resultado en pantalla (es probable que obtengas resultados diferentes
si repites el experimento):
1073909760
1075061012
1205
1074091790
1073941880
Evidentemente, no son cinco ceros.
Podemos inicializar todos los valores de un vector a cero con un bucle for:
inicializados a cero.c inicializados a cero.c
1 #include <stdio.h>
2
3 #define TALLA 10
4
5 int main(void)
6 {
7 int i, a[TALLA];
8
9 for (i = 0; i < TALLA; i++)
10 a[i] = 0;
11
12 for (i = 0; i < TALLA; i++)
13 printf ("%dn", a[i]);
14
15 return 0;
16 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 67 Declara e inicializa un vector de 100 elementos de modo que los componentes de ´ındice
par valgan 0 y los de ´ındice impar valgan 1.
· 68 Escribe un programa C que almacene en un vector los 50 primeros n´umeros de Fibonacci.
Una vez calculados, el programa los mostrar´a por pantalla en orden inverso.
· 69 Escribe un programa C que almacene en un vector los 50 primeros n´umeros de Fibonacci.
Una vez calculados, el programa pedir´a al usuario que introduzca un n´umero y dir´a si es o no
es uno de los 50 primeros n´umeros de Fibonacci.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Hay una forma alternativa de inicializar vectores. En este fragmento se definen e inicializan
dos vectores, uno con todos sus elementos a 0 y otro con una secuencia ascendente de n´umeros:
1 #define TALLA 5
2 ...
3 int a[TALLA] = {0, 0, 0, 0, 0};
4 int b[TALLA] = {1, 2, 3, 4, 5};
Ten en cuenta que, al declarar e inicializar simult´aneamente un vector, debes indicar expl´ıci-
tamente los valores del vector y, por tanto, esta aproximaci´on s´olo es factible para la inicializa-
ci´on de unos pocos valores.
2.1.3. Un programa de ejemplo: la criba de Erat´ostenes
Vamos a ilustrar lo aprendido desarrollando un sencillo programa que calcule y muestre los
n´umeros primos menores que N, para un valor de N fijo y determinado en el propio programa.
Usaremos un m´etodo denominado la criba de Erat´ostenes, uno de los algoritmos m´as antiguos y
que debemos a un astr´onomo, ge´ografo, matem´atico y fil´osofo de la antigua Grecia. El m´etodo
utiliza un vector de N valores booleanos (unos o ceros). Si la celda de ´ındice i contiene el valor 1,
Introducci´on a la Programaci´on con C 65
2.1 Vectores est´aticos
Cuesti´on de estilo: ¿constantes o literales al declarar la talla de un vector?
¿Por qu´e se prefiere declarar el tama˜no de los vectores con constantes en lugar de con literales
de entero? Porque la talla del vector puede aparecer en diferentes puntos del programa y
es posible que alg´un d´ıa hayamos de modificar el programa para trabajar con un vector de
talla diferente. En tal caso, nos ver´ıamos obligados a editar muchas l´ıneas diferentes del
programa (puede que decenas o cientos). Bastar´ıa con que olvid´asemos modificar una o con
que modific´asemos una de m´as para que el programa fuera err´oneo. F´ıjate en este programa
C:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i, a[10], b[10];
6
7 for (i = 0; i < 10; i++)
8 a[i] = 0;
9 for (i = 0; i < 10; i++)
10 b[i] = 0;
11 for (i = 0; i < 10; i++)
12 printf ("%dn", a[i]);
13 for (i = 0; i < 10; i++)
14 printf ("%dn", b[i]);
15 return 0;
16 }
Las tallas de los vectores a y b aparecen en seis lugares diferentes: en sus declaraciones,
en los bucles que los inicializan y en los que se imprimen. Imagina que deseas modificar
el programa para que a pase a tener 20 enteros: tendr´as que modificar s´olo tres de esos
dieces. Ello te obliga a leer el programa detenidamente y, cada vez que encuentres un diez,
pararte a pensar si ese diez en particular corresponde o no a la talla de a. Innecesariamente
complicado. Estudia esta alternativa:
1 #include <stdio.h>
2
3 #define TALLA_A 10
4 #define TALLA_B 10
5
6 int main(void)
7 {
8 int i, a[TALLA_A], b[TALLA_B];
9
10 for (i = 0; i < TALLA_A; i++)
11 a[i] = 0;
12 for (i = 0; i < TALLA_B; i++)
13 b[i] = 0;
14 for (i = 0; i < TALLA_A; i++)
15 printf ("%dn", a[i]);
16 for (i = 0; i < TALLA_B; i++)
17 printf ("%dn", b[i]);
18 return 0;
19 }
Si ahora necesitas modificar a para que tenga 20 elementos, basta con que edites la l´ınea
3 sustituyendo el 10 por un 20. Mucho m´as r´apido y con mayor garant´ıa de no cometer
errores.
¿Por qu´e en Python no nos preocup´o esta cuesti´on? Recuerda que en Python no hab´ıa
declaraci´on de variables, que las listas pod´ıan modificar su longitud durante la ejecuci´on de
los programas y que pod´ıas consultar la longitud de cualquier secuencia de valores con la
funci´on predefinida len. Python ofrece mayores facilidades al programador, pero a un doble
precio: la menor velocidad de ejecuci´on y el mayor consumo de memoria.
66 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
Omisi´on de talla en declaraciones con inicializaci´on y otro modo de inicializar
Tambi´en puedes declarar e inicializar vectores as´ı:
int a[] = {0, 0, 0, 0, 0};
int b[] = {1, 2, 3, 4, 5};
El compilador deduce que la talla del vector es 5, es decir, el n´umero de valores que aparecen
a la derecha del igual. Te recomendamos que, ahora que est´as aprendiendo, no uses esta
forma de declarar vectores: siempre que puedas, opta por una que haga expl´ıcito el tama˜no
del vector.
En C99 es posible inicializar s´olo algunos valores del vector. La sintaxis es un poco
enrevesada. Aqu´ı tienes un ejemplo en el que s´olo inicializamos el primer y ´ultimo elementos
de un vector de talla 10:
int a[] = {[0] = 0, [9] = 0};
consideramos que i es primo, y si no, que no lo es. Inicialmente, todas las celdas excepto la de
´ındice 0 valen 1. Entonces ((tachamos)) (ponemos un 0 en) las celdas cuyo ´ındice es m´ultiplo
de 2. Acto seguido se busca la siguiente casilla que contiene un 1 y se procede a tachar todas
las casillas cuyo ´ındice es m´ultiplo del ´ındice de esta casilla. Y as´ı sucesivamente. Cuando se ha
recorrido completamente el vector, las casillas cuyo ´ındice es primo contienen un 1.
Vamos con una primera versi´on del programa:
eratostenes.c eratostenes.c
1 #include <stdio.h>
2
3 #define N 100
4
5 int main(void)
6 {
7 int criba[N], i, j;
8
9 /* Inicializaci´on */
10 criba[0] = 0;
11 for (i=1; i<N; i++)
12 criba[i] = 1;
13
14 /* Criba de Erat´ostenes */
15 for (i=2; i<N; i++)
16 if (criba[i])
17 for (j=2; i*j<N; j++)
18 criba[i*j] = 0;
19
20 /* Mostrar los resultados */
21 for (i=0; i<N; i++)
22 if (criba[i])
23 printf ("%dn", i);
24
25 return 0;
26 }
Observa que hemos tenido que decidir qu´e valor toma N, pues el vector criba debe tener un
tama˜no conocido en el momento en el que se compila el programa. Si deseamos conocer los,
digamos, primos menores que 200, tenemos que modificar la l´ınea 3.
Mejoremos el programa. ¿Es necesario utilizar 4 bytes para almacenar un 0 o un 1? Estamos
malgastando memoria. Esta otra versi´on reduce a una cuarta parte el tama˜no del vector criba:
eratostenes 1.c eratostenes.c
1 #include <stdio.h>
2
Introducci´on a la Programaci´on con C 67
2.1 Vectores est´aticos
3 #define N 100
4
5 int main(void)
6 {
7 char criba[N];
8 int i, j;
9
10 /* Inicializaci´on */
11 criba[0] = 0;
12 for (i=1; i<N; i++)
13 criba[i] = 1;
14
15 /* Criba de Erat´ostenes */
16 for (i=2; i<N; i++)
17 if (criba[i])
18 for (j=2; i*j<N; j++)
19 criba[i*j] = 0;
20
21 /* Mostrar los resultados */
22 for (i=0; i<N; i++)
23 if (criba[i])
24 printf ("%dn", i);
25
26 return 0;
27 }
Mejor as´ı.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 70 Puedes ahorrar tiempo de ejecuci´on haciendo que i tome valores entre 2 y la ra´ız cuadrada
de N. Modifica el programa y comprueba que obtienes el mismo resultado.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.1.4. Otro programa de ejemplo: estad´ısticas
Queremos efectuar estad´ısticas con una serie de valores (las edades de 15 personas), as´ı que
vamos a dise˜nar un programa que nos ayude. En una primera versi´on, solicitaremos las edades
de todas las personas y, a continuaci´on, calcularemos y mostraremos por pantalla la edad
media, la desviaci´on t´ıpica, la moda y la mediana. Las f´ormulas para el c´alculo de la media y
la desviaci´on t´ıpica de n elementos son:
¯x =
n
i=1 xi
n
,
σ =
n
i=1(xi − ¯x)2
n
,
donde xi es la edad del individuo n´umero i.2
La moda es la edad que m´as veces aparece (si dos
o m´as edades aparecen muchas veces con la m´axima frecuencia, asumiremos que una cualquiera
de ellas es la moda). La mediana es la edad tal que el 50% de las edades son inferiores o iguales
a ella y el restante 50% son mayores o iguales.
Empezamos por la declaraci´on del vector que albergar´a las 15 edades y por leer los datos:
edades.c edades.c
1 #include <stdio.h>
2
3 #define PERSONAS 15
4
5 int main(void)
6 {
7 int edad[PERSONAS], i;
8
2Hay una definici´on alternativa de la desviaci´on t´ıpica en la que el denominador de la fracci´on es n − 1.
68 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
Optimiza, pero no te pases
C permite optimizar mucho los programas y hacer que estos consuman la menor memoria
posible o que se ejecuten a mucha velocidad gracias a una adecuada selecci´on de operaciones.
En el programa de la criba de Erat´ostenes, por ejemplo, a´un podemos reducir m´as el consumo
de memoria: para representar un 1 o un 0 basta un solo bit. Como en un char caben 8 bits,
podemos proponer esta otra soluci´on:
eratostenes bit.c eratostenes bit.c
1 #include <stdio.h>
2 #include <math.h>
3
4 #define N 100
5
6 int main(void)
7 {
8 char criba[N/8+1]; // Ocupa unas 8 veces menos que la versi´on anterior.
9 int i, j;
10
11 /* Inicializaci´on */
12 criba[0] = 254; // Pone todos los bits a 1 excepto el primero.
13 for (i=1; i<=N/8; i++)
14 criba[i] = 255; // Pone todos los bits a 1.
15
16 /* Criba de Erat´ostenes */
17 for (i=2; i<N; i++)
18 if (criba[i/8] & (1 << (i%8))) // Pregunta si el bit en posici´on i vale 1.
19 for (j=2; i*j<N; j++)
20 criba[i*j/8] &= ~(1 << ((i*j) % 8)); // Pone a 0 el bit en posici´on i*j.
21
22 /* Mostrar los resultados */
23 for (i=0; i<N; i++)
24 if (criba[i/8] & 1 << (i%8))
25 printf ("%dn", i);
26
27 return 0;
28 }
¡Buf! La legibilidad deja mucho que desear. Y no s´olo eso: consultar si un determinado bit
vale 1 y fijar un determinado bit a 0 resultan ser operaciones m´as costosas que consultar
si el valor de un char es 1 o, respectivamente, fijar el valor de un char a 0, pues debes
hacerlo mediante operaciones de divisi´on entera, resto de divisi´on entera, desplazamiento,
negaci´on de bits y el operador &.
¿Vale la pena reducir la memoria a una octava parte si, a cambio, el programa pierde
legibilidad y, adem´as, resulta m´as lento? No hay una respuesta definitiva a esta pregunta. La
´unica respuesta es: depende. En seg´un qu´e aplicaciones, puede resultar necesario, en otras
no. Lo que no debes hacer, al menos de momento, es obsesionarte con la optimizaci´on y
complicar innecesariamente tus programas.
9 /* Lectura de edades */
10 for (i=0; i<PERSONAS; i++) {
11 printf ("Por favor, introduce edad de la persona n´umero %d: ", i+1);
12 scanf ("%d", &edad[i]);
13 }
14
15 return 0;
16 }
Vale la pena que te detengas a observar c´omo indicamos a scanf que lea la celda de ´ındice i en
el vector edad: usamos el operador & delante de la expresi´on edad[i]. Es lo que cab´ıa esperar:
edad[i] es un escalar de tipo int, y ya sabes que scanf espera su direcci´on de memoria.
Pasamos ahora a calcular la edad media y la desviaci´on t´ıpica (no te ha de suponer dificultad
alguna con la experiencia adquirida al aprender Python):
Introducci´on a la Programaci´on con C 69
2.1 Vectores est´aticos
edades 1.c edades.c
1 #include <stdio.h>
2 #include <math.h>
3
4 #define PERSONAS 15
5
6 int main(void)
7 {
8 int edad[PERSONAS], i, suma_edad ;
9 float suma_desviacion, media, desviacion ;
10
11 /* Lectura de edades */
12 for (i=0; i<PERSONAS; i++) {
13 printf ("Por favor, introduce edad de la persona n´umero %d: ", i+1);
14 scanf ("%d", &edad[i]);
15 }
16
17 /* C´alculo de la media */
18 suma_edad = 0;
19 for (i=0; i<PERSONAS; i++)
20 suma_edad += edad[i];
21 media = suma_edad / (float) PERSONAS;
22
23 /* C´alculo de la desviacion t´ıpica */
24 suma_desviacion = 0.0;
25 for (i=0; i<PERSONAS; i++)
26 suma_desviacion += (edad[i] - media) * (edad[i] - media);
27 desviacion = sqrt( suma_desviacion / PERSONAS );
28
29 /* Impresi´on de resultados */
30 printf ("Edad media : %fn", media);
31 printf ("Desv. t´ıpica: %fn", desviacion);
32
33 return 0;
34 }
El c´alculo de la moda (la edad m´as frecuente) resulta m´as problem´atica. ¿C´omo abordar el
c´alculo? Vamos a presentar dos versiones diferentes. Empezamos por una que consume dema-
siada memoria. Dado que trabajamos con edades, podemos asumir que ninguna edad iguala o
supera los 150 a˜nos. Podemos crear un vector con 150 contadores, uno para cada posible edad:
edades 2.c edades.c
1 #include <stdio.h>
2 #include <math.h>
3
4 #define PERSONAS 15
5 #define MAX_EDAD 150
6
7 int main(void)
8 {
9 int edad[PERSONAS], i, suma_edad;
10 float suma_desviacion, media, desviacion;
11 int contador[MAX_EDAD], frecuencia, moda;
12
13 /* Lectura de edades */
14 for (i=0; i<PERSONAS; i++) {
15 printf ("Por favor, introduce edad de la persona n´umero %d: ", i+1);
16 scanf ("%d", &edad[i]);
17 }
18
19 /* C´alculo de la media */
20 suma_edad = 0;
21 for (i=0; i<PERSONAS; i++)
22 suma_edad += edad[i];
70 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
23 media = suma_edad / (float) PERSONAS;
24
25 /* C´alculo de la desviacion t´ıpica */
26 suma_desviacion = 0.0;
27 for (i=0; i<PERSONAS; i++)
28 suma_desviacion += (edad[i] - media) * (edad[i] - media);
29 desviacion = sqrt( suma_desviacion / PERSONAS );
30
31 /* C´alculo de la moda */
32 for (i=0; i<MAX_EDAD; i++) // Inicializaci´on de los contadores.
33 contador[i] = 0;
34 for (i=0; i<PERSONAS; i++)
35 contador[edad[i]]++; // Incrementamos el contador asociado a edad[i].
36 moda = -1;
37 frecuencia = 0;
38 for (i=0; i<MAX_EDAD; i++) // B´usqueda de la moda (edad con mayor valor del contador).
39 if (contador[i] > frecuencia) {
40 frecuencia = contador[i];
41 moda = i;
42 }
43
44 /* Impresi´on de resultados */
45 printf ("Edad media : %fn", media);
46 printf ("Desv. t´ıpica: %fn", desviacion);
47 printf ("Moda : %dn", moda);
48
49 return 0;
50 }
Esta soluci´on consume un vector de 150 elementos enteros cuando no es estrictamente ne-
cesario. Otra posibilidad pasa por ordenar el vector de edades y contar la longitud de cada
secuencia de edades iguales. La edad cuya secuencia sea m´as larga es la moda:
edades 3.c edades.c
1 #include <stdio.h>
2 #include <math.h>
3
4 #define PERSONAS 15
5
6 int main(void)
7 {
8 int edad[PERSONAS], i, j, aux , suma_edad;
9 float suma_desviacion, media, desviacion;
10 int moda, frecuencia, frecuencia_moda ;
11
12 /* Lectura de edades */
13 for (i=0; i<PERSONAS; i++) {
14 printf ("Por favor, introduce edad de la persona n´umero %d: ", i+1);
15 scanf ("%d", &edad[i]);
16 }
17
18 /* C´alculo de la media */
19 suma_edad = 0;
20 for (i=0; i<PERSONAS; i++)
21 suma_edad += edad[i];
22 media = suma_edad / (float) PERSONAS;
23
24 /* C´alculo de la desviacion t´ıpica */
25 suma_desviacion = 0.0;
26 for (i=0; i<PERSONAS; i++)
27 suma_desviacion += (edad[i] - media) * (edad[i] - media);
28 desviacion = sqrt( suma_desviacion / PERSONAS );
29
30 /* C´alculo de la moda */
Introducci´on a la Programaci´on con C 71
2.1 Vectores est´aticos
31 for (i=0; i<PERSONAS-1; i++) // Ordenaci´on mediante burbuja.
32 for (j=0; j<PERSONAS-i; j++)
33 if (edad[j] > edad[j+1]) {
34 aux = edad[j];
35 edad[j] = edad[j+1];
36 edad[j+1] = aux;
37 }
38
39 frecuencia = 0;
40 frecuencia_moda = 0;
41 moda = -1;
42 for (i=0; i<PERSONAS-1; i++) // B´usqueda de la serie de valores id´enticos m´as larga.
43 if (edad[i] == edad[i+1]) {
44 frecuencia++;
45 if (frecuencia > frecuencia_moda) {
46 frecuencia_moda = frecuencia;
47 moda = edad[i];
48 }
49 }
50 else
51 frecuencia = 0;
52
53 /* Impresi´on de resultados */
54 printf ("Edad media : %fn", media);
55 printf ("Desv. t´ıpica: %fn", desviacion);
56 printf ("Moda : %dn", moda);
57
58 return 0;
59 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 71 ¿Contiene en cada instante la variable frecuencia el verdadero valor de la frecuencia de
aparici´on de un valor? Si no es as´ı, ¿qu´e contiene? ¿Afecta eso al c´alculo efectuado? ¿Por qu´e?
· 72 Esta nueva versi´on del programa presenta la ventaja adicional de no fijar un l´ımite
m´aximo a la edad de las personas. El programa resulta, as´ı, de aplicaci´on m´as general. ¿Son
todo ventajas? ¿Ves alg´un aspecto negativo? Reflexiona sobre la velocidad de ejecuci´on del
programa comparada con la del programa que consume m´as memoria.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
S´olo nos resta calcular la mediana. Mmmm. No hay que hacer nuevos c´alculos para conocer
la mediana: gracias a que hemos ordenado el vector, la mediana es el valor que ocupa la posici´on
central del vector, es decir, la edad de ´ındice PERSONAS/2.
edades 4.c edades.c
1 #include <stdio.h>
2 #include <math.h>
3
4 #define PERSONAS 15
5
6 int main(void)
7 {
8 int edad[PERSONAS], i, j, aux, suma_edad;
9 float suma_desviacion, media, desviacion;
10 int moda, frecuencia, frecuencia_moda, mediana ;
11
12 /* Lectura de edades */
13 for (i=0; i<PERSONAS; i++) {
14 printf ("Por favor, introduce edad de la persona n´umero %d: ", i+1);
15 scanf ("%d", &edad[i]);
16 }
17
18 /* C´alculo de la media */
19 suma_edad = 0;
72 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
20 for (i=0; i<PERSONAS; i++)
21 suma_edad += edad[i];
22 media = suma_edad / (float) PERSONAS;
23
24 /* C´alculo de la desviacion t´ıpica */
25 suma_desviacion = 0.0;
26 for (i=0; i<PERSONAS; i++)
27 suma_desviacion += (edad[i] - media) * (edad[i] - media);
28 desviacion = sqrt( suma_desviacion / PERSONAS );
29
30 /* C´alculo de la moda */
31 for (i=0; i<PERSONAS-1; i++) // Ordenaci´on mediante burbuja.
32 for (j=0; j<PERSONAS-i; j++)
33 if (edad[j] > edad[j+1]) {
34 aux = edad[j];
35 edad[j] = edad[j+1];
36 edad[j+1] = aux;
37 }
38
39 frecuencia = 0;
40 frecuencia_moda = 0;
41 moda = -1;
42 for (i=0; i<PERSONAS-1; i++)
43 if (edad[i] == edad[i+1])
44 if (++frecuencia > frecuencia_moda) { // Ver ejercicio 73.
45 frecuencia_moda = frecuencia;
46 moda = edad[i];
47 }
48 else
49 frecuencia = 0;
50
51 /* C´alculo de la mediana */
52 mediana = edad[PERSONAS/2]
53
54 /* Impresi´on de resultados */
55 printf ("Edad media : %fn", media);
56 printf ("Desv. t´ıpica: %fn", desviacion);
57 printf ("Moda : %dn", moda);
58 printf ("Mediana : %dn", mediana);
59
60 return 0;
61 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 73 F´ıjate en la l´ınea 44 del programa y comp´arala con las l´ıneas 44 y 45 de su versi´on
anterior. ¿Es correcto ese cambio? ¿Lo ser´ıa este otro?:
44 if (frecuencia++ > frecuencia_moda) {
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Bueno, vamos a modificar ahora el programa para que el usuario introduzca cuantas edades
desee hasta un m´aximo de 20. Cuando se introduzca un valor negativo para la edad, entende-
remos que ha finalizado la introducci´on de datos.
edades.c
1 #include <stdio.h>
2 #include <math.h>
3
4 #define PERSONAS 20
5
6 int main(void)
7 {
8 int edad[PERSONAS], i, j, aux, suma_edad;
9 float suma_desviacion, media, desviacion;
Introducci´on a la Programaci´on con C 73
2.1 Vectores est´aticos
10 int moda, frecuencia, frecuencia_moda, mediana;
11
12 /* Lectura de edades */
13 for (i=0; i<PERSONAS; i++) {
14 printf ("Introduce edad de la persona %d (si es negativa, acaba): ", i+1);
15 scanf ("%d", &edad[i]);
16 if (edad[i] < 0)
17 break;
18 }
19
20 ...
21
22 return 0;
23 }
Mmmm. Hay un problema: si no damos 20 edades, el vector presentar´a toda una serie de valores
sin inicializar y, por tanto, con valores arbitrarios. Ser´ıa un grave error tomar esos valores por
edades introducidas por el usuario. Una buena idea consiste en utilizar una variable entera que
nos diga en todo momento cu´antos valores introdujo realmente el usuario en el vector edad:
edades 5.c edades.c
1 #include <stdio.h>
2 #include <math.h>
3
4 #define MAX_PERSONAS 20
5
6 int main(void)
7 {
8 int edad[MAX_PERSONAS], personas , i, j, aux, suma_edad;
9 float suma_desviacion, media, desviacion;
10 int moda, frecuencia, frecuencia_moda, mediana;
11
12 /* Lectura de edades */
13 personas = 0;
14 for (i=0; i<MAX_PERSONAS; i++) {
15 printf ("Introduce edad de la persona %d (si es negativa, acabar): ", i+1);
16 scanf ("%d", &edad[i]);
17 if (edad[i] < 0)
18 break;
19 personas++;
20 }
21
22 ...
23
24 return 0;
25 }
La constante que hasta ahora se llamaba PERSONAS ha pasado a llamarse MAX_PERSONAS. Se
pretende reflejar que su valor es la m´axima cantidad de edades de personas que podemos
manejar, pues el n´umero de edades que manejamos realmente pasa a estar en la variable entera
personas.
Una forma alternativa de hacer lo mismo nos permite prescindir del ´ındice i:
edades 6.c edades.c
1 #include <stdio.h>
2 #include <math.h>
3
4 #define MAX_PERSONAS 20
5
6 int main(void)
7 {
8 int edad[MAX_PERSONAS], personas, j, aux, suma_edad;
9 float suma_desviacion, media, desviacion;
10 int moda, frecuencia, frecuencia_moda, mediana;
74 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
11
12 /* Lectura de edades */
13 personas = 0;
14 do {
15 printf ("Introduce edad de la persona %d (si es negativa, acabar): ", personas+1);
16 scanf ("%d", &edad[personas]);
17 personas++;
18 } while (personas < MAX_PERSONAS && edad[personas-1] >= 0);
19 personas--;
20
21 ...
22
23 return 0;
24 }
Imagina que se han introducido edades de 10 personas. La variable personas apunta (con-
ceptualmente) al final de la serie de valores que hemos de considerar para efectuar los c´alculos
pertinentes:
edad 6
0
18
1
30
2
18
3
19
4
19
5
31
6
1
7
27
8
66
9
-1
10
887
11
-55
12
0
13
391
14
0
15
-6
16
89
17
322
18
-2
19
10personas
20MAX PERSONAS
Ya podemos calcular la edad media, pero con un cuidado especial por las posibles divisiones
por cero que provocar´ıa que el usuario escribiera una edad negativa como edad de la primera
persona (en cuyo caso personas valdr´ıa 0):
edades 7.c edades.c
1 #include <stdio.h>
2 #include <math.h>
3
4 #define MAX_PERSONAS 20
5
6 int main(void)
7 {
8 int edad[MAX_PERSONAS], personas, i, j, aux, suma_edad;
9 float suma_desviacion, media, desviacion;
10 int moda, frecuencia, frecuencia_moda, mediana;
11
12 /* Lectura de edades */
13 personas = 0;
14 do {
15 printf ("Introduce edad de la persona %d (si es negativa, acabar): ", personas+1);
16 scanf ("%d", &edad[personas]);
17 personas++;
18 } while (personas < MAX_PERSONAS && edad[personas-1] >= 0);
19 personas--;
20
21 if (personas > 0) {
22 /* C´alculo de la media */
23 suma_edad = 0;
24 for (i=0; i<personas ; i++)
25 suma_edad += edad[i];
26 media = suma_edad / (float) personas ;
27
28 /* C´alculo de la desviacion t´ıpica */
29 suma_desviacion = 0.0;
30 for (i=0; i<personas ; i++)
31 suma_desviacion += (edad[i] - media) * (edad[i] - media);
32 desviacion = sqrt( suma_desviacion / personas );
33
Introducci´on a la Programaci´on con C 75
2.1 Vectores est´aticos
34 /* C´alculo de la moda */
35 for (i=0; i<personas -1; i++) // Ordenaci´on mediante burbuja.
36 for (j=0; j<personas -i; j++)
37 if (edad[j] > edad[j+1]) {
38 aux = edad[j];
39 edad[j] = edad[j+1];
40 edad[j+1] = aux;
41 }
42
43 frecuencia = 0;
44 frecuencia_moda = 0;
45 moda = -1;
46 for (i=0; i<personas -1; i++)
47 if (edad[i] == edad[i+1])
48 if (++frecuencia > frecuencia_moda) {
49 frecuencia_moda = frecuencia;
50 moda = edad[i];
51 }
52 else
53 frecuencia = 0;
54
55 /* C´alculo de la mediana */
56 mediana = edad[personas /2];
57
58 /* Impresi´on de resultados */
59 printf ("Edad media : %fn", media);
60 printf ("Desv. t´ıpica: %fn", desviacion);
61 printf ("Moda : %dn", moda);
62 printf ("Mediana : %dn", mediana);
63 }
64 else
65 printf ("No se introdujo dato alguno.n");
66
67 return 0;
68 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 74 Cuando el n´umero de edades es par no hay elemento central en el vector ordenado, as´ı
que estamos escogiendo la mediana como uno cualquiera de los elementos ((centrales)). Utiliza
una definici´on alternativa de edad mediana que considera que su valor es la media de las dos
edades que ocupan las posiciones m´as pr´oximas al centro.
· 75 Modifica el ejercicio anterior para que, caso de haber dos o m´as valores con la m´axima
frecuencia de aparici´on, se muestren todos por pantalla al solicitar la moda.
· 76 Modifica el programa anterior para que permita efectuar c´alculos con hasta 100 personas.
· 77 Modifica el programa del ejercicio anterior para que muestre, adem´as, cu´antas edades
hay entre 0 y 9 a˜nos, entre 10 y 19, entre 20 y 29, etc. Considera que ninguna edad es igual o
superior a 150.
Ejemplo: si el usuario introduce las siguientes edades correspondientes a 12 personas:
10 23 15 18 20 18 57 12 29 31 78 28
el programa mostrar´a (adem´as de la media, desviaci´on t´ıpica, moda y mediana), la siguiente
tabla:
0 - 9: 0
10 - 19: 5
20 - 29: 4
30 - 39: 1
40 - 49: 0
50 - 59: 1
60 - 69: 0
70 - 79: 1
76 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
80 - 89: 0
90 - 99: 0
100 - 109: 0
110 - 119: 0
120 - 129: 0
130 - 139: 0
140 - 149: 0
· 78 Modifica el programa para que muestre un histograma de edades. La tabla anterior se
mostrar´a ahora como este histograma:
0 - 9:
10 - 19: *****
20 - 29: ****
30 - 39: *
40 - 49:
50 - 59: *
60 - 69:
70 - 79: *
80 - 89:
90 - 99:
100 - 109:
110 - 119:
120 - 129:
130 - 139:
140 - 149:
Como puedes ver, cada asterisco representa la edad de una persona.
· 79 Modifica el programa anterior para que el primer y ´ultimo rangos de edades mostrados
en el histograma correspondan a tramos de edades en los que hay al menos una persona. El
histograma mostrado antes aparecer´a ahora as´ı:
10 - 19: *****
20 - 29: ****
30 - 39: *
40 - 49:
50 - 59: *
60 - 69:
70 - 79: *
· 80 Modifica el programa del ejercicio anterior para que muestre el mismo histograma de
esta otra forma:
| ######### | | | | | | |
| ######### | ######### | | | | | |
| ######### | ######### | | | | | |
| ######### | ######### | | | | | |
| ######### | ######### | ######### | | ######### | | ######### |
+-----------+-----------+-----------+-----------+-----------+-----------+-----------+
| 10 - 19 | 20 - 29 | 30 - 39 | 40 - 49 | 50 - 59 | 60 - 69 | 70 - 79 |
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.1.5. Otro programa de ejemplo: una calculadora para polinomios
Deseamos implementar una calculadora para polinomios de grado menor o igual que 10. Un
polinomio p(x) = p0 + p1x + p2x2
+ p3x3
+ · · · + p10x10
puede representarse con un vector en el
que se almacenan sus 11 coeficientes (p0, p1, . . . , p10). Vamos a construir un programa C que
permita leer por teclado dos polinomios p(x) y q(x) y, una vez le´ıdos, calcule los polinomios
s(x) = p(x) + q(x) y m(x) = p(x) · q(x).
Empezaremos definiendo dos vectores p y q que han de poder contener 11 valores en coma
flotante:
Introducci´on a la Programaci´on con C 77
2.1 Vectores est´aticos
polinomios.c
1 #include <stdio.h>
2 #define TALLA_POLINOMIO 11
3
4 int main(void)
5 {
6 float p[TALLA_POLINOMIO], q[TALLA_POLINOMIO];
7 ...
Como leer por teclado 11 valores para p y 11 m´as para q es innecesario cuando trabajamos
con polinomios de grado menor que 10, nuestro programa leer´a los datos pidiendo en primer
lugar el grado de cada uno de los polinomios y solicitando ´unicamente el valor de los coeficientes
de grado menor o igual que el indicado:
E polinomios.c E
1 #include <stdio.h>
2
3 #define TALLA_POLINOMIO 11
4
5 int main(void)
6 {
7 float p[TALLA_POLINOMIO], q[TALLA_POLINOMIO];
8 int grado;
9 int i;
10
11 /* Lectura de p */
12 do {
13 printf ("Grado de p (entre 0 y %d): ", TALLA_POLINOMIO-1); scanf ("%d", &grado);
14 } while (grado < 0 || grado >= TALLA_POLINOMIO);
15 for (i = 0; i<=grado; i++) {
16 printf ("p %d: ", i); scanf ("%f", &p[i]);
17 }
18
19 /* Lectura de q */
20 do {
21 printf ("Grado de q (entre 0 y %d): ", TALLA_POLINOMIO-1); scanf ("%d", &grado);
22 } while (grado < 0 || grado >= TALLA_POLINOMIO);
23 for (i = 0; i<=grado; i++) {
24 printf ("q %d: ", i); scanf ("%f", &q[i]);
25 }
26
27 return 0;
28 }
El programa presenta un problema: no inicializa los coeficientes que correponden a los
t´erminos xn
, para n mayor que el grado del polinomio. Como dichos valores deben ser nu-
los, hemos de inicializarlos expl´ıcitamente (en aras de la brevedad mostramos ´unicamente la
inicializaci´on de los coeficientes de p):
polinomios.c
4 ...
5 int main(void)
6 {
7 float p[TALLA_POLINOMIO], q[TALLA_POLINOMIO];
8 int grado;
9 int i;
10
11 /* Lectura de p */
12 do {
13 printf ("Grado de p (entre 0 y %d): ", TALLA_POLINOMIO-1); scanf ("%d", &grado);
14 } while (grado < 0 || grado >= TALLA_POLINOMIO);
15 for (i = 0; i<=grado; i++) {
16 printf ("p %d: ", i); scanf ("%f", &p[i]);
17 }
78 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
18 for (i=grado+1; i<TALLA_POLINOMIO; i++)
19 p[i] = 0.0;
20 ...
21 return 0;
22 }
Ahora que hemos le´ıdo los polinomios, calculemos la suma. La almacenaremos en un nuevo
vector llamado s. La suma de dos polinomios de grado menor que TALLA_POLINOMIO es un
polinomio de grado tambi´en menor que TALLA_POLINOMIO, as´ı que el vector s tendr´a talla
TALLA_POLINOMIO.
polinomios.c
4 ...
5 int main(void)
6 {
7 float p[TALLA_POLINOMIO], q[TALLA_POLINOMIO], s[TALLA_POLINOMIO];
8 ...
El procedimiento para calcular la suma de polinomios es sencillo. He aqu´ı el c´alculo y la
presentaci´on del resultado en pantalla:
polinomios.c polinomios.c
1 #include <stdio.h>
2
3 #define TALLA_POLINOMIO 11
4
5 int main(void)
6 {
7 float p[TALLA_POLINOMIO], q[TALLA_POLINOMIO], s[TALLA_POLINOMIO];
8 int grado;
9 int i;
10
11 /* Lectura de p */
12 do {
13 printf ("Grado de p (entre 0 y %d): ", TALLA_POLINOMIO-1); scanf ("%d", &grado);
14 } while (grado < 0 || grado >= TALLA_POLINOMIO);
15 for (i = 0; i<=grado; i++) {
16 printf ("p %d: ", i); scanf ("%f", &p[i]);
17 }
18 for (i=grado+1; i<TALLA_POLINOMIO; i++)
19 p[i] = 0.0;
20
21 /* Lectura de q */
22 do {
23 printf ("Grado de q (entre 0 y %d): ", TALLA_POLINOMIO-1); scanf ("%d", &grado);
24 } while (grado < 0 || grado >= TALLA_POLINOMIO);
25 for (i = 0; i<=grado; i++) {
26 printf ("q %d: ", i); scanf ("%f", &q[i]);
27 }
28 for (i=grado+1; i<TALLA_POLINOMIO; i++)
29 q[i] = 0.0;
30
31 /* C´alculo de la suma */
32 for (i=0; i<TALLA_POLINOMIO; i++)
33 s[i] = p[i] + q[i];
34
35 /* Presentaci´on del resultado */
36 printf ("Suma: %f ", s[0]);
37 for (i=1; i<TALLA_POLINOMIO; i++)
38 printf ("+ %f x^%d ", s[i], i);
39 printf ("n");
40
41 return 0;
42 }
Introducci´on a la Programaci´on con C 79
2.1 Vectores est´aticos
Aqu´ı tienes un ejemplo de uso del programa con los polinomios p(x) = 5 + 3x + 5x2
+ x3
y
q(x) = 4 − 4x − 5x2
+ x3
:
Grado de p (entre 0 y 10): 3
p_0: 5
p_1: 3
p_2: 5
p_3: 1
Grado de q (entre 0 y 10): 3
q_0: 4
q_1: -4
q_2: -5
q_3: 1
Suma: 9.000000 + -1.000000 x^1 + 0.000000 x^2 + 2.000000 x^3 + 0.000000 x^4 +
0.000000 x^5 + 0.000000 x^6 + 0.000000 x^7 + 0.000000 x^8 + 0.000000 x^9 +
0.000000 x^10
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 81 Modifica el programa anterior para que no se muestren los coeficientes nulos.
· 82 Tras efectuar los cambios propuestos en el ejercicio anterior no aparecer´a nada por
pantalla cuando todos los valores del polinomio sean nulos. Modifica el programa para que, en
tal caso, se muestre por pantalla 0.000000.
· 83 Tras efectuar los cambios propuestos en los ejercicios anteriores, el polinomio empieza
con un molesto signo positivo cuando s0 es nulo. Corrige el programa para que el primer t´ermino
del polinomio no sea precedido por el car´acter +.
· 84 Cuando un coeficiente es negativo, por ejemplo −1, el programa anterior muestra su
correspondiente t´ermino en pantalla as´ı: + -1.000 x^1. Modifica el programa anterior para
que un t´ermino con coeficiente negativo como el del ejemplo se muestre as´ı: - 1.000000 x^1.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Nos queda lo m´as dif´ıcil: el producto de los dos polinomios. Lo almacenaremos en un vector
llamado m. Como el producto de dos polinomios de grado menor o igual que n es un polinomio
de grado menor o igual que 2n, la talla del vector m no es TALLA_POLINOMIO:
1 ...
2 int main(void)
3 {
4 float p[TALLA_POLINOMIO], q[TALLA_POLINOMIO], s[TALLA_POLINOMIO];
5 float m[2*TALLA_POLINOMIO-1];
6 ...
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 85 ¿Entiendes por qu´e hemos reservado 2*TALLA_POLINOMIO-1 elementos para m y no
2*TALLA_POLINOMIO?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
El coeficiente mi, para valores de i entre 0 y el grado m´aximo de m(x), es decir, entre los
enteros 0 y 2*TALLA_POLINOMIO-2, se calcula as´ı:
mi =
i
j=0
pj · qi−j.
Deberemos tener cuidado de no acceder err´oneamente a elementos de p o q fuera del rango de
´ındices v´alidos.
Implementemos ese c´alculo:
polinomios 1.c polinomios.c
1 #include <stdio.h>
2
3 #define TALLA_POLINOMIO 11
4
80 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
5 int main(void)
6 {
7 float p[TALLA_POLINOMIO], q[TALLA_POLINOMIO], s[TALLA_POLINOMIO];
8 float m[2*TALLA_POLINOMIO-1];
9 int grado;
10 int i, j;
11
12 /* Lectura de p */
13 do {
14 printf ("Grado de p (entre 0 y %d): ", TALLA_POLINOMIO-1); scanf ("%d", &grado);
15 } while (grado < 0 || grado >= TALLA_POLINOMIO);
16 for (i = 0; i<=grado; i++) {
17 printf ("p %d: ", i); scanf ("%f", &p[i]);
18 }
19 for (i=grado+1; i<TALLA_POLINOMIO; i++)
20 p[i] = 0.0;
21
22 /* Lectura de q */
23 do {
24 printf ("Grado de q (entre 0 y %d): ", TALLA_POLINOMIO-1); scanf ("%d", &grado);
25 } while (grado < 0 || grado >= TALLA_POLINOMIO);
26 for (i = 0; i<=grado; i++) {
27 printf ("q %d: ", i); scanf ("%f", &q[i]);
28 }
29 for (i=grado+1; i<TALLA_POLINOMIO; i++)
30 q[i] = 0.0;
31
32 /* C´alculo de la suma */
33 for (i=0; i<TALLA_POLINOMIO; i++)
34 s[i] = p[i] + q[i];
35
36 /* Presentaci´on del resultado */
37 printf ("Suma: %f ", s[0]);
38 for (i=1; i<TALLA_POLINOMIO; i++)
39 printf ("+ %f x^%d ", s[i], i);
40 printf ("n");
41
42 /* C´alculo del producto */
43 for (i=0; i<2*TALLA_POLINOMIO-1; i++) {
44 m[i] = 0.0;
45 for (j=0; j<=i; j++)
46 if (j < TALLA_POLINOMIO && i-j < TALLA_POLINOMIO)
47 m[i] += p[j] * q[i-j];
48 }
49
50 /* Presentaci´on del resultado */
51 printf ("Producto: %f ", m[0]);
52 for (i=1; i<2*TALLA_POLINOMIO-1; i++)
53 printf ("+ %f x^%d ", m[i], i);
54 printf ("n");
55
56 return 0;
57 }
Observa que nos hubiera venido bien definir sendas funciones para la lectura y escritura de
los polinomios, pero al no saber definir funciones todav´ıa, hemos tenido que copiar dos veces el
fragmento de programa correspondiente.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 86 El programa que hemos dise˜nado es ineficiente. Si, por ejemplo, trabajamos con polino-
mios de grado 5, sigue operando con los coeficientes correspondientes a x6
, x7
,. . . , x10
, que son
nulos. Modifica el programa para que, con la ayuda de variables enteras, recuerde el grado de
los polinomios p(x) y q(x) en sendas variables talla_p y talla_q y use esta informaci´on en los
c´alculos de modo que se opere ´unicamente con los coeficientes de los t´erminos de grado menor
Introducci´on a la Programaci´on con C 81
2.1 Vectores est´aticos
o igual que el grado del polinomio.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Ahora que hemos presentado tres programas ilustrativos del uso de vectores en C, f´ıjate en
que:
El tama˜no de los vectores siempre se determina en tiempo de compilaci´on.
En un vector podemos almacenar una cantidad de elementos menor o igual que la decla-
rada en su capacidad, nunca mayor.
Si almacenamos menos elementos de los que caben (como en el programa que efect´ua
estad´ısticas de una serie de edades), necesitas alguna variable auxiliar que te permita saber
en todo momento cu´antas de las celdas contienen informaci´on. Si a˜nades un elemento, has
de incrementar t´u mismo el valor de esa variable.
Ya sabes lo suficiente sobre vectores para poder hacer frente a estos ejercicios:
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 87 Dise˜na un programa que pida el valor de 10 n´umeros enteros distintos y los almacene en
un vector. Si se da el caso, el programa advertir´a al usuario, tan pronto sea posible, si introduce
un n´umero repetido y solicitar´a nuevamente el n´umero hasta que sea diferente de todos los
anteriores. A continuaci´on, el programa mostrar´a los 10 n´umeros por pantalla.
· 88 En una estaci´on meteorol´ogica registramos la temperatura (en grados cent´ıgrados) cada
hora durante una semana. Almacenamos el resultado en un vector de 168 componentes (que es
el resultado del producto 7 × 24). Dise˜na un programa que lea los datos por teclado y muestre:
La m´axima y m´ınima temperaturas de la semana.
La m´axima y m´ınima temperaturas de cada d´ıa.
La temperatura media de la semana.
La temperatura media de cada d´ıa.
El n´umero de d´ıas en los que la temperatura media fue superior a 30 grados.
El d´ıa y hora en que se registr´o la mayor temperatura.
· 89 La cabecera stdlib.h incluye la declaraci´on de funciones para generar n´umeros aleato-
rios. La funci´on rand, que no tiene par´ametros, devuelve un entero positivo aleatorio. Si deseas
generar n´umeros aleatorios entre 0 y un valor dado N, puedes evaluar rand() % (N+1). Cuando
ejecutas un programa que usa rand, la semilla del generador de n´umeros aleatorios es siempre
la misma, as´ı que acabas obteniendo la misma secuencia de n´umeros aleatorios. Puedes cambiar
la semilla del generador de n´umeros aleatorios pas´andole a la funci´on srand un n´umero entero
sin signo.
Usa el generador de n´umeros aleatorios para inicializar un vector de 10 elementos con
n´umeros enteros entre 0 y 4. Muestra por pantalla el resultado. Detecta y muestra, a con-
tinuaci´on, el tama˜no de la sucesi´on m´as larga de n´umeros consecutivos iguales.
(Ejemplo: si los n´umeros generados son 0 4 3 3 2 1 3 2 2 2, el tramo m´as largo formado
por n´umeros iguales es de talla 3 (los tres doses al final de la secuencia), as´ı que por pantalla
aparecer´a el valor 3.)
· 90 Modifica el ejercicio anterior para que trabaje con un vector de 100 elementos.
· 91 Genera un vector con 20 n´umeros aleatorios entre 0 y 100 y muestra por pantalla el
vector resultante y la secuencia de n´umeros crecientes consecutivos m´as larga.
(Ejemplo: la secuencia 1 33 73 85 87 93 99 es la secuencia creciente m´as larga en la serie
de n´umeros 87 45 34 12 1 33 73 85 87 93 99 0 100 65 32 17 29 16 12 0.)
· 92 Escribe un programa C que ejecute 1000 veces el c´alculo de la longitud de la secuencia
m´as larga sobre diferentes secuencias aleatorias (ver ejercicio anterior) y que muestre la longitud
media y desviaci´on t´ıpica de dichas secuencias.
· 93 Genera 100 n´umeros aleatorios entre 0 y 1000 y almac´enalos en un vector. Determina
a continuaci´on qu´e n´umeros aparecen m´as de una vez.
82 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
· 94 Genera 100 n´umeros aleatorios entre 0 y 10 y almac´enalos en un vector. Determina a
continuaci´on cu´al es el n´umero que aparece m´as veces.
· 95 Dise˜na un programa C que almacene en un vector los 100 primeros n´umeros primos.
· 96 Dise˜na un programa C que lea y almacene en un vector 10 n´umeros enteros asegur´andose
de que sean positivos. A continuaci´on, el programa pedir´a que se introduzca una serie de n´umeros
enteros y nos dir´a si cada uno de ellos est´a o no en el vector. El programa finaliza cuando el
usuario introduce un n´umero negativo.
· 97 Dise˜na un programa C que lea y almacene en un vector 10 n´umeros enteros asegur´andose
de que sean positivos. A continuaci´on, el programa pedir´a que se introduzca una serie de n´umeros
enteros y nos dir´a si cada uno de ellos est´a o no en el vector. El programa finaliza cuando el
usuario introduce un n´umero negativo.
Debes ordenar por el m´etodo de la burbuja el vector de 10 elementos tan pronto se conocen
sus valores. Cuando debas averiguar si un n´umero est´a o no en el vector, utiliza el algoritmo de
b´usqueda dicot´omica.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.1.6. Disposici´on de los vectores en memoria
Es importante que conozcas bien c´omo se disponen los vectores en memoria. Cuando se encuen-
tra esta declaraci´on en un programa:
int a[5];
el compilador reserva una zona de memoria contigua capaz de albergar 5 valores de tipo int.
Como una variable de tipo int ocupa 4 bytes, el vector a ocupar´a 20 bytes.
Podemos comprobarlo con este programa:
1 #include <stdio.h>
2
3 #define TALLA 5
4
5 int main(void)
6 {
7 int a[TALLA];
8
9 printf ("Ocupaci´on de un elemento de a (en bytes): %dn", sizeof(a[0]));
10 printf ("Ocupaci´on de a (en bytes): %dn", sizeof(a));
11 return 0;
12 }
El resultado de ejecutarlo es ´este:
Ocupaci´on de un elemento de a (en bytes): 4
Ocupaci´on de a (en bytes): 20
Cada byte de la memoria tiene una direcci´on. Si, pongamos por caso, el vector a empieza
en la direcci´on 1000, a[0] se almacena en los bytes 1000–1003, a[1] en los bytes 1004–1007, y
as´ı sucesivamente. El ´ultimo elemento, a[4], ocupar´a los bytes 1016–1019:
996:
1000: a[0]
1004: a[1]
1008: a[2]
1012: a[3]
1016: a[4]
1020:
Introducci´on a la Programaci´on con C 83
2.1 Vectores est´aticos
Big-endian y little-endian
Lo bueno de los est´andares es. . . que hay muchos donde elegir. No hay forma de ponerse de
acuerdo. Muchos ordenadores almacenan los n´umeros enteros de m´as de 8 bits disponiendo
los bits m´as significativos en la direcci´on de memoria m´as baja y otros, en la m´as alta. Los
primeros se dice que siguen la codificaci´on ((big-endian)) y los segundos, ((little-endian)).
Pongamos un ejemplo. El n´umero 67586 se representa en binario con cuatro bytes:
00000000 00000001 00001000 00000010
Supongamos que ese valor se almacena en los cuatro bytes que empiezan en la direcci´on
1000. En un ordenador ((big-endian)), se dispondr´ıan en memoria as´ı (te indicamos bajo cada
byte su direcci´on de memoria):
1000: 00000000 00000001 00001000 00000010
1000 1001 1002 1003
En un ordenador ((little-endian)), por contra, se representar´ıa de esta otra forma:
1000: 00000010 00001000 00000001 00000000
1000 1001 1002 1003
Los ordenadores PC (que usan microprocesadores Intel y AMD), por ejemplo, son ((little-
endian)) y los Macintosh basados en microprocesadores Motorola son ((big-endian)). Aunque
nosotros trabajamos en clase con ordenadores Intel, te mostraremos los valores binarios
como est´as acostumbrado a verlos: con el byte m´as significativo a la izquierda.
La diferente codificaci´on de unas y otras plataformas plantea serios problemas a la hora
de intercambiar informaci´on en ficheros binarios, es decir, ficheros que contienen volcados
de la informaci´on en memoria. Nos detendremos nuevamente sobre esta cuesti´on cuando
estudiamos ficheros.
Por cierto, lo de ((little-endian)) y ((big-endian)) viene de ((Los viajes de Gulliver)), la novela
de Johnathan Swift. En ella, los liliputienses debaten sobre una importante cuesti´on pol´ıtica:
¿deben abrirse los huevos pasados por agua por su extremo grande, como defiende el partido
Big-Endian, o por su extremo puntiagudo, como mantiene el partido Little-Endian?
¿Recuerdas el operador & que te presentamos en el cap´ıtulo anterior? Es un operador unario
que permite conocer la direcci´on de memoria de una variable. Puedes aplicar el operador & a
un elemento del vector. Por ejemplo, &a[2] es la direcci´on de memoria en la que empieza a[2],
es decir, la direcci´on 1008 en el ejemplo.
Veamos qu´e direcci´on ocupa cada elemento de un vector cuando ejecutamos un programa
sobre un computador real:
direcciones vector.c direcciones vector.c
1 #include <stdio.h>
2
3 #define TALLA 5
4
5 int main(void)
6 {
7 int a[TALLA], i;
8
9 for (i = 0; i < TALLA; i++)
10 printf ("Direcci´on de a[%d]: %un", i, (unsigned int) &a[i]);
11
12 return 0;
13 }
Al ejecutar el programa obtenemos en pantalla lo siguiente (puede que obtengas un resultado
diferente si haces la prueba t´u mismo, pues el vector puede estar en un lugar cualquiera de la
memoria):
Direcci´on de a[0]: 3221222640
Direcci´on de a[1]: 3221222644
Direcci´on de a[2]: 3221222648
84 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
Direcci´on de a[3]: 3221222652
Direcci´on de a[4]: 3221222656
¿Ves? Cada direcci´on de memoria de una celda de a se diferencia de la siguiente en 4
unidades.
Recuerda que la funci´on de lectura de datos por teclado scanf modifica el valor de una
variable cuya direcci´on de memoria se le suministra. Para depositar en la zona de memoria de
la variable el nuevo valor necesita conocer la direcci´on de memoria. Por esa raz´on preced´ıamos
los identificadores de las variables con el operador &. Este programa, por ejemplo, lee por teclado
el valor de todos los componentes de un vector utilizando el operador & para conocer la direcci´on
de memoria de cada uno de ellos:
lee vector.c lee vector.c
1 #include <stdio.h>
2
3 #define TALLA 5
4
5 int main(void)
6 {
7 int a[TALLA], i;
8
9 for (i = 0; i < TALLA; i++)
10 printf ("Introduce el valor de a[%d]:", i); scanf ("%d", &a[i]);
11
12 return 0;
13 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 98 ¿Qu´e problema presenta esta otra versi´on del mismo programa?
lee vector 1.c lee vector.c
1 #include <stdio.h>
2
3 #define TALLA 5
4
5 int main(void)
6 {
7 int a[TALLA], i;
8
9 for (i = 0; i < TALLA; i++)
10 printf ("Introduce el valor de a[%d]:", i); scanf ("%d", a[i]);
11
12 return 0;
13 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Analiza este programa:
direcciones vector2.c direcciones vector2.c
1 #include <stdio.h>
2
3 #define TALLA 5
4
5 int main(void)
6 {
7 int a[TALLA], i;
8
9 for (i = 0; i < TALLA; i++)
10 printf ("Direcci´on de a[%d]: %un", i, (unsigned int) &a[i]);
11
12 printf ("Direcci´on de a: %un", (unsigned int) a);
13 return 0;
14 }
He aqu´ı el resultado de ejecutarlo:
Introducci´on a la Programaci´on con C 85
2.1 Vectores est´aticos
Direcci´on de a[0]: 3221222640
Direcci´on de a[1]: 3221222644
Direcci´on de a[2]: 3221222648
Direcci´on de a[3]: 3221222652
Direcci´on de a[4]: 3221222656
Direcci´on de a: 3221222640
Observa que la direcci´on de memoria de las l´ıneas primera y ´ultima es la misma. En conse-
cuencia, esta l´ınea:
1 printf ("Direcci´on de a: %un", (unsigned int) &a[0]);
es equivalente a esta otra:
1 printf ("Direcci´on de a: %un", (unsigned int) a);
As´ı pues, a expresa una direcci´on de memoria (la de su primer elemento), es decir, a es un puntero
o referencia a memoria y es equivalente a &a[0]. La caracter´ıstica de que el identificador de un
vector represente, a la vez, al vector y a un puntero que apunta donde empieza el vector recibe
el nombre dualidad vector-puntero, y es un rasgo propio del lenguaje de programaci´on C.
Representaremos esquem´aticamente los vectores de modo similar a como represent´abamos
las listas en Python:
a 0
0
0
1
0
2
0
3
0
4
F´ıjate en que el gr´afico pone claramente de manifiesto que a es un puntero, pues se le representa
con una flecha que apunta a la zona de memoria en la que se almacenan los elementos del vector.
Nos interesa dise˜nar programas con un nivel de abstracci´on tal que la imagen conceptual que
tengamos de los vectores se limite a la del diagrama.
Mentiremos cada vez menos
Lo cierto es que a no es exactamente un puntero, aunque funciona como tal. Ser´ıa m´as
justo representar la memoria as´ı:
a 0
0
0
1
0
2
0
3
0
4
Pero, por el momento, conviene que consideres v´alida la representaci´on en la que a es un
puntero. Cuando estudiemos la gesti´on de memoria din´amica abundaremos en esta cuesti´on.
Recuerda que el operador & obtiene la direcci´on de memoria en la que se encuentra un valor.
En esta figura te ilustramos &a[0] y &a[2] como sendos punteros a sus respectivas celdas en el
vector.
&a[2]
a 0
0
0
1
0
2
0
3
0
4
&a[0]
¿C´omo ((encuentra)) C la direcci´on de memoria de un elemento del vector cuando accedemos a
trav´es de un ´ındice? Muy sencillo, efectuando un c´alculo consistente en sumar al puntero que
se˜nala el principio del vector el resultado de multiplicar el ´ındice por el tama˜no de un elemento
del vector. La expresi´on a[2], por ejemplo, se entiende como ((accede al valor de tipo int que
empieza en la direcci´on a con un desplazamiento de 2 × 4 bytes)). Una sentencia de asignaci´on
como a[2] = 0 se interpreta como ((almacena el valor 0 en el entero int que empieza en la
direcci´on de memoria de a m´as 2 × 4 bytes)).
86 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
2.1.7. Algunos problemas de C: accesos il´ıcitos a memoria
Aqu´ı tienes un programa con un resultado que puede sorprenderte:
ilicito.c ilicito.c
1 #include <stdio.h>
2
3 #define TALLA 3
4
5 int main(void)
6 {
7 int v[TALLA], w[TALLA], i;
8
9 for(i=0; i<TALLA; i++) {
10 v[i] = i;
11 w[i] = 10 + i;
12 }
13
14 printf ("+--------+----------------------+-------+n");
15 printf ("| Objeto | Direcci´on de memoria | Valor |n");
16 printf ("+--------+----------------------+-------+n");
17 printf ("| i | %20u | %5d |n", (unsigned int) &i, i);
18 printf ("+--------+----------------------+-------+n");
19 printf ("| w[0] | %20u | %5d |n", (unsigned int) &w[0], w[0]);
20 printf ("| w[1] | %20u | %5d |n", (unsigned int) &w[1], w[1]);
21 printf ("| w[2] | %20u | %5d |n", (unsigned int) &w[2], w[2]);
22 printf ("+--------+----------------------+-------+n");
23 printf ("| v[0] | %20u | %5d |n", (unsigned int) &v[0], v[0]);
24 printf ("| v[1] | %20u | %5d |n", (unsigned int) &v[1], v[1]);
25 printf ("| v[2] | %20u | %5d |n", (unsigned int) &v[2], v[2]);
26 printf ("+--------+----------------------+-------+n");
27 printf ("| v[-2] | %20u | %5d |n", (unsigned int) &v[-2], v[-2]);
28 printf ("| v[-3] | %20u | %5d |n", (unsigned int) &v[-3], v[-3]);
29 printf ("| v[-4] | %20u | %5d |n", (unsigned int) &v[-4], v[-4]);
30 printf ("| w[5] | %20u | %5d |n", (unsigned int) &w[5], w[5]);
31 printf ("| w[-1] | %20u | %5d |n", (unsigned int) &w[-1], w[-1]);
32 printf ("| v[-5] | %20u | %5d |n", (unsigned int) &v[-5], v[-5]);
33 printf ("+--------+----------------------+-------+n");
34
35 return 0;
36 }
Aqu´ı tienes el resultado de su ejecuci´on3
:
+--------+----------------------+-------+
| Objeto | Direcci´on de memoria | Valor |
+--------+----------------------+-------+
| i | 3221222636 | 3 |
+--------+----------------------+-------+
| w[0] | 3221222640 | 10 |
| w[1] | 3221222644 | 11 |
| w[2] | 3221222648 | 12 |
+--------+----------------------+-------+
| v[0] | 3221222656 | 0 |
| v[1] | 3221222660 | 1 |
| v[2] | 3221222664 | 2 |
+--------+----------------------+-------+
| v[-2] | 3221222648 | 12 |
| v[-3] | 3221222644 | 11 |
| v[-4] | 3221222640 | 10 |
| w[5] | 3221222660 | 1 |
3Nuevamente, una advertencia: puede que obtengas un resultado diferente al ejecutar el programa en tu
ordenador. La asignaci´on de direcciones de memoria a cada objeto de un programa es una decisi´on que adopta
el compilador con cierta libertad.
Introducci´on a la Programaci´on con C 87
2.1 Vectores est´aticos
| w[-1] | 3221222636 | 3 |
| v[-5] | 3221222636 | 3 |
+--------+----------------------+-------+
La salida es una tabla con tres columnas: en la primera se indica el objeto que se est´a
estudiando, la segunda corresponde a la direcci´on de memoria de dicho objeto4
y la tercera
muestra el valor almacenado en dicho objeto. A la vista de las direcciones de memoria de los
objetos i, v[0], v[1], v[2], w[0], w[1] y w[2], el compilador ha reservado la memoria de
estas variables as´ı:
3221222636: i3
3221222640: w[0]10
3221222644: w[1]11
3221222648: w[2]12
3221222652:
3221222656: v[0]0
3221222660: v[1]1
3221222664: v[2]2
F´ıjate en que las seis ´ultimas filas de la tabla corresponden a accesos a v y w con ´ındices
fuera de rango. Cuando trat´abamos de acceder a un elemento inexistente en una lista Python,
el int´erprete generaba un error de tipo (error de ´ındice). Ante una situaci´on similar, C no
detecta error alguno. ¿Qu´e hace, pues? Aplica la f´ormula de indexaci´on, sin m´as. Estudiemos
con calma el primer caso extra˜no: v[-2]. C lo interpreta como: ((acceder al valor almacenado en
la direcci´on que resulta de sumar 3221222656 (que es donde empieza el vector v) a (−2)×4 (−2
es el ´ındice del vector y 4 es tama˜no de un int))). Haz el c´alculo: el resultado es 3221222648. . .
¡la misma direcci´on de memoria que ocupa el valor de w[2]! Esa es la raz´on de que se muestre
el valor 12. En la ejecuci´on del programa, v[-2] y w[2] son exactamente lo mismo. Encuentra
t´u mismo una explicaci´on para los restantes accesos il´ıcitos.
¡Ojo! Que se pueda hacer no significa que sea aconsejable hacerlo. En absoluto. Es m´as:
debes evitar acceder a elementos con ´ındices de vector fuera de rango. Si no conviene hacer
algo as´ı, ¿por qu´e no comprueba C si el ´ındice est´a en el rango correcto antes de acceder a los
elementos y, en caso contrario, nos se˜nala un error? Por eficiencia. Un programa que maneje
vectores acceder´a a sus elementos, muy probablemente, en numerosas ocasiones. Si se ha de
comprobar si el ´ındice est´a en el rango de valores v´alidos, cada acceso se penalizar´a con un
par de comparaciones y el programa se ejecutar´a m´as lentamente. C sacrifica seguridad por
velocidad, de ah´ı que tenga cierta fama (justificad´ısma) de lenguaje ((peligroso)).
2.1.8. Asignaci´on y copia de vectores
Este programa pretende copiar un vector en otro, pero es incorrecto:
copia vectores mal.c E copia vectores mal.c E
1 #define TALLA 10
2
3 int main(void)
4 {
5 int original[TALLA] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10} ;
6 int copia[TALLA];
7
8 copia = original;
9
10 return 0;
11 }
4Si ejecutas el programa en tu ordenador, es probable que obtengas valores distintos para las direcciones de
memoria. Es normal: en cada ordenador y con cada ejecuci´on se puede reservar una zona de memoria distinta
para los datos.
88 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
Violaci´on de segmento
Los errores de acceso a zonas de memoria no reservada se cuentan entre los peores. En
el ejemplo, hemos accedido a la zona de memoria de un vector sali´endonos del rango de
indexaci´on v´alido de otro, lo cual ha producido resultados desconcertantes.
Pero podr´ıa habernos ido a´un peor: si tratas de escribir en una zona de memoria que
no pertenece a ninguna de tus variables, cosa que puedes hacer asignando un valor a un
elemento de vector fuera de rango, es posible que se genere una excepci´on durante la
ejecuci´on del programa: intentar escribir en una zona de memoria que no ha sido asignada
a nuestro proceso dispara, en Unix, una se˜nal de ((violaci´on de segmento)) (segmentation
violation) que provoca la inmediata finalizaci´on de la ejecuci´on del programa. F´ıjate en este
programa:
violacion.c E violacion.c E
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a[10];
6
7 a[10000] = 1;
8
9 return 0;
10 }
Cuando lo ejecutamos en un ordenador bajo Unix, obtenemos este mensaje por pantalla:
Violaci´on de segmento
El programa ha finalizado abruptamente al ejecutar la asignaci´on de la l´ınea 7.
Estos errores en la gesti´on de memoria se manifiestan de formas muy variadas: pue-
den producir resultados extra˜nos, finalizar la ejecuci´on incorrectamente o incluso bloquear
al computador. ¿Bloquear al computador? S´ı, en sistemas operativos poco robustos, como
Microsoft Windows, el ordenador puede quedarse bloqueado. (Probablemente has experi-
mentado la sensaci´on usando algunos programas comerciales en el entorno Microsoft Win-
dows.) Ello se debe a que ciertas zonas de memoria deber´ıan estar fuera del alcance de
los programas de usuario y el sistema operativo deber´ıa prohibir accesos il´ıcitos. Unix mata
al proceso que intenta efectuar accesos il´ıcitos (de ah´ı que terminen con mensajes como
((Violaci´on de segmento))). Microsoft Windows no tiene la precauci´on de protegerlas, as´ı que
las consecuencias son mucho peores.
Pero casi lo peor es que tu programa puede funcionar mal en unas ocasiones y bien en
otras. El hecho de que el programa pueda funcionar mal algunas veces y bien el resto es
peligros´ısimo: como los errores pueden no manifestarse durante el desarrollo del programa,
cabe la posibilidad de que no los detectes. Nada peor que dar por bueno un programa que,
en realidad, es incorrecto.
Tenlo siempre presente: la gesti´on de vectores obliga a estar siempre pendiente de no
rebasar la zona de memoria reservada.
Si compilas el programa, obtendr´as un error en la l´ınea 8 que te impedir´a obtener un ejecuta-
ble: ((incompatible types in assignment)). El mensaje de error nos indica que no es posible
efectuar asignaciones entre tipos vectoriales.
Nuestra intenci´on era que antes de ejecutar la l´ınea 8, la memoria presentara este aspecto:
original 1
0
2
1
3
2
4
3
5
4
6
5
7
6
8
7
9
8
10
9
copia
0 1 2 3 4 5 6 7 8 9
y, una vez ejecutada la l´ınea 8 llegar a una de estas dos situaciones:
1. obtener en copia una copia del contenido de original:
Introducci´on a la Programaci´on con C 89
2.1 Vectores est´aticos
original 1
0
2
1
3
2
4
3
5
4
6
5
7
6
8
7
9
8
10
9
copia 1
0
2
1
3
2
4
3
5
4
6
5
7
6
8
7
9
8
10
9
2. o conseguir que, como en Python, copia apunte al mismo lugar que original:
original 1
0
2
1
3
2
4
3
5
4
6
5
7
6
8
7
9
8
10
9
copia
0 1 2 3 4 5 6 7 8 9
Pero no ocurre ninguna de las dos cosas: el identificador de un vector est´atico se considera un
puntero inmutable. Siempre apunta a la misma direcci´on de memoria. No puedes asignar un
vector a otro porque eso significar´ıa cambiar el valor de su direcci´on. (Observa, adem´as, que en
el segundo caso, la memoria asignada a copia quedar´ıa sin puntero que la referenciara.)
Si quieres copiar el contenido de un vector en otro debes hacerlo elemento a elemento:
copia vectores.c copia vectores.c
1 #define TALLA 10
2
3 int main(void)
4 {
5 int original[TALLA] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10} ;
6 int copia[TALLA];
7 int i;
8
9 for (i=0; i<TALLA; i++)
10 copia[i] = original[i];
11
12 return 0;
13 }
2.1.9. Comparaci´on de vectores
En Python pod´ıamos comparar listas. Por ejemplo, [1,2,3] == [1,1+1,3] devolv´ıa True. Ya
lo habr´as adivinado: C no permite comparar vectores. Efectivamente.
Si quieres comparar dos vectores, has de hacerlo elemento a elemento:
compara vectores.c compara vectores.c
1 #define TALLA 3
2
3 int main(void)
4 {
5 int original[TALLA] = { 1, 2, 3 };
6 int copia[TALLA] = {1, 1+1, 3};
7 int i, son_iguales;
8
9 son_iguales = 1; // Suponemos que todos los elementos son iguales dos a dos.
10 i = 0;
11 while (i < TALLA && son_iguales) {
12 if (copia[i] != original[i]) // Pero basta con que dos elementos no sean iguales...
13 son_iguales = 0; // ... para que los vectores sean distintos.
14 i++;
15 }
16
17 if (son_iguales)
18 printf ("Son igualesn");
19 else
20 printf ("No son igualesn");
90 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
21
22 return 0;
23 }
2.2. Cadenas est´aticas
Las cadenas son un tipo de datos b´asico en Python, pero no en C. Las cadenas de C son vectores
de caracteres (elementos de tipo char) con una peculiaridad: el texto de la cadena termina
siempre en un car´acter nulo. El car´acter nulo tiene c´odigo ASCII 0 y podemos representarlo
tanto con el entero 0 como con el car´acter ’0’ (recuerda que ’0’ es una forma de escribir el
valor entero 0). ¡Ojo! No confundas ’0’ con ’0’: el primero vale 0 y el segundo vale 48.
Las cadenas est´aticas en C son, a diferencia de las cadenas Python, mutables. Eso significa
que puedes modificar el contenido de una cadena durante la ejecuci´on de un programa.
2.2.1. Declaraci´on de cadenas
Las cadenas se declaran como vectores de caracteres, as´ı que debes proporcionar el n´umero
m´aximo de caracteres que es capaz de almacenar: su capacidad. Esta cadena, por ejemplo, se
declara con capacidad para almacenar 10 caracteres:
char a[10];
Puedes inicializar la cadena con un valor en el momento de su declaraci´on:
char a[10] = "cadena";
Hemos declarado a como un vector de 10 caracteres y lo hemos inicializado asign´andole la
cadena "cadena". F´ıjate: hemos almacenado en a una cadena de menos de 10 caracteres. No
hay problema: la longitud de la cadena almacenada en a es menor que la capacidad de a.
2.2.2. Representaci´on de las cadenas en memoria
A simple vista, "cadena" ocupa 6 bytes, pues contamos en ella 6 caracteres, pero no es as´ı.
En realidad, "cadena" ocupa 7 bytes: los 6 que corresponden a los 6 caracteres que ves m´as
uno correspondiente a un car´acter nulo al final, que se denomina terminador de cadena y es
invisible.
Al declarar e inicializar una cadena as´ı:
char a[10] = "cadena";
la memoria queda de este modo:
a c
0
a
1
d
2
e
3
n
4
a
5
0
6 7 8 9
Es decir, es como si hubi´esemos inicializado la cadena de este otro modo equivalente:
1 char a[10] = { ’c’, ’a’, ’d’, ’e’, ’n’, ’a’, ’0’ };
Recuerda, pues, que hay dos valores relacionados con el tama˜no de una cadena:
su capacidad, que es la talla del vector de caracteres;
su longitud, que es el n´umero de caracteres que contiene, sin contar el terminador de la
cadena. La longitud de la cadena debe ser siempre estrictamente menor que la capacidad
del vector para no desbordar la memoria reservada.
¿Y por qu´e toda esta complicaci´on del terminador de cadena? Lo normal al trabajar con una
variable de tipo cadena es que su longitud var´ıe conforme evoluciona la ejecuci´on del programa,
pero el tama˜no de un vector es fijo. Por ejemplo, si ahora tenemos en a el texto "cadena" y
m´as tarde decidimos guardar en ella el texto "texto", que tiene un car´acter menos, estaremos
pasando de esta situaci´on:
Introducci´on a la Programaci´on con C 91
2.2 Cadenas est´aticas
Una cadena de longitud uno no es un car´acter
Hemos dicho en el cap´ıtulo anterior que una cadena de un s´olo car´acter, por ejemplo "y ",
no es lo mismo que un car´acter, por ejemplo ’y’. Ahora puedes saber por qu´e: la diferencia
estriba en que "y " ocupa dos bytes, el que corresponde al car´acter ’y’ y el que corresponde
al car´acter nulo ’0’, mientras que ’y’ ocupa un solo byte.
F´ıjate en esta declaraci´on de variables:
1 char a = ’y’;
2 char b[2] = "y";
He aqu´ı una representaci´on gr´afica de las variables y su contenido:
ya
b y
0
0
1
Recuerda:
Las comillas simples definen un car´acter y un car´acter ocupa un solo byte.
Las comillas dobles definen una cadena. Toda cadena incluye un car´acter nulo invisible
al final.
a c
0
a
1
d
2
e
3
n
4
a
5
0
6 7 8 9
a esta otra:
a t
0
e
1
x
2
t
3
o
4
0
5 6 7 8 9
F´ıjate en que la zona de memoria asignada a a sigue siendo la misma. El ((truco)) del terminador
ha permitido que la cadena decrezca. Podemos conseguir tambi´en que crezca a voluntad. . . pero
siempre que no se rebase la capacidad del vector.
Hemos representado las celdas a la derecha del terminador como cajas vac´ıas, pero no es
cierto que lo est´en. Lo normal es que contengan valores arbitrarios, aunque eso no importa
mucho: el convenio de que la cadena termina en el primer car´acter nulo hace que el resto de
caracteres no se tenga en cuenta. Es posible que, en el ejemplo anterior, la memoria presente
realmente este aspecto:
a t
0
e
1
x
2
t
3
o
4
0
5
a
6
u
7
0
8
x
9
Por comodidad representaremos las celdas a la derecha del terminador con cajas vac´ıas, pues
no importa en absoluto lo que contienen.
¿Qu´e ocurre si intentamos inicializar una zona de memoria reservada para s´olo 10 chars con
una cadena de longitud mayor que 9?
char a[10] = "supercalifragilisticoespialidoso"; //
!
Mal!
Estaremos cometiendo un grav´ısimo error de programaci´on que, posiblemente, no detecte el
compilador. Los caracteres que no caben en a se escriben en la zona de memoria que sigue a la
zona ocupada por a.
a s
0
u
1
p
2
e
3
r
4
c
5
a
6
l
7
i
8
f
9
r a g i l i s t i c o e s p i a l i d o s o 0
Ya vimos en un apartado anterior las posibles consecuencias de ocupar memoria que no nos ha
sido reservada: puede que modifiques el contenido de otras variables o que trates de escribir en
una zona que te est´a vetada, con el consiguiente aborto de la ejecuci´on del programa.
92 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
Como resulta que en una variable con capacidad para, por ejemplo, 80 caracteres s´olo
caben realmente 79 caracteres aparte del nulo, adoptaremos una curiosa pr´actica al declarar
variables de cadena que nos permitir´a almacenar los 80 caracteres (adem´as del nulo) sin crear
una constante confusi´on con respecto al n´umero de caracteres que caben en ellas:
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char cadena[MAXLON+1]; /* Reservamos 81 caracteres: 80 caracteres m´as el terminador */
8
9 return 0;
10 }
2.2.3. Entrada/salida de cadenas
Las cadenas se muestran con printf y la adecuada marca de formato sin que se presenten
dificultades especiales. Lo que s´ı resulta problem´atico es leer cadenas. La funci´on scanf presenta
una seria limitaci´on: s´olo puede leer ((palabras)), no ((frases)). Ello nos obligar´a a presentar una
nueva funci´on (gets). . . que se lleva fatal con scanf .
Salida con printf
Empecemos por considerar la funci´on printf , que muestra cadenas con la marca de formato %s.
Aqu´ı tienes un ejemplo de uso:
salida cadena.c salida cadena.c
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char cadena[MAXLON+1] = "una cadena";
8
9 printf ("El valor de cadena es %s.n", cadena);
10
11 return 0;
12 }
Al ejecutar el programa obtienes en pantalla esto:
El valor de cadena es una cadena.
Puedes alterar la presentaci´on de la cadena con modificadores:
salida cadena con modificadores.c salida cadena con modificadores.c
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char cadena[MAXLON+1] = "una cadena";
8
9 printf ("El valor de cadena es (%s).n", cadena);
10 printf ("El valor de cadena es (%20s).n", cadena);
11 printf ("El valor de cadena es (%-20s).n", cadena);
12
13 return 0;
14 }
Introducci´on a la Programaci´on con C 93
2.2 Cadenas est´aticas
El valor de cadena es (una cadena).
El valor de cadena es ( una cadena).
El valor de cadena es (una cadena ).
¿Y si deseamos mostrar una cadena car´acter a car´acter? Podemos hacerlo llamando a printf
sobre cada uno de los caracteres, pero recuerda que la marca de formato asociada a un car´acter
es %c:
salida caracter a caracter.c salida caracter a caracter.c
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char cadena[MAXLON+1] = "una cadena";
8 int i;
9
10 i = 0;
11 while (cadena[i] != ’0’) {
12 printf ("%cn", cadena[i]);
13 i++;
14 }
15
16 return 0;
17 }
Este es el resultado de la ejecuci´on:
u
n
a
c
a
d
e
n
a
Entrada con scanf
Poco m´as hay que contar acerca de printf . La funci´on scanf es un reto mayor. He aqu´ı un ejemplo
que pretende leer e imprimir una cadena en la que podemos guardar hasta 80 caracteres (sin
contar el terminador nulo):
lee una cadena.c lee una cadena.c
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char cadena[MAXLON+1];
8
9 scanf ("%s", cadena);
10 printf ("La cadena le´ıda es %sn", cadena);
11
12 return 0;
13 }
¡Ojo! ¡No hemos puesto el operador & delante de cadena! ¿Es un error? No. Con las cadenas
no hay que poner el car´acter & del identificador al usar scanf . ¿Por qu´e? Porque scanf espera
94 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
una direcci´on de memoria y el identificador, por la dualidad vector-puntero, ¡es una direcci´on
de memoria!
Recuerda: cadena[0] es un char, pero cadena, sin m´as, es la direcci´on de memoria en la
que empieza el vector de caracteres.
Ejecutemos el programa e introduzcamos una palabra:
una
La cadena le´ıda es una
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 99 ¿Es v´alida esta otra forma de leer una cadena? Pru´ebala en tu ordenador.
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char cadena[MAXLON+1];
8
9 scanf ("%s", &cadena[0]);
10 printf ("La cadena le´ıda es %s.n", cadena);
11
12 return 0;
13 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Cuando scanf recibe el valor asociado a cadena, recibe una direcci´on de memoria y, a partir
de ella, deja los caracteres le´ıdos de teclado. Debes tener en cuenta que si los caracteres le´ıdos
exceden la capacidad de la cadena, se producir´a un error de ejecuci´on.
¿Y por qu´e printf no muestra por pantalla una simple direcci´on de memoria cuando ejecuta-
mos la llamada printf ("La cadena le´ıda es %s.n", cadena)? Si es cierto lo dicho, cadena
es una direcci´on de memoria. La explicaci´on es que la marca %s es interpretada por printf como
((me pasan una direcci´on de memoria en la que empieza una cadena, as´ı que he de mostrar su
contenido car´acter a car´acter hasta encontrar un car´acter nulo)).
Lectura con gets
Hay un problema pr´actico con scanf : s´olo lee una ((palabra)), es decir, una secuencia de caracteres
no blancos. Hagamos la prueba:
lee frase mal.c E lee frase mal.c E
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char cadena[MAXLON+1];
8
9 scanf ("%s", cadena);
10 printf ("La cadena le´ıda es %s.n", cadena);
11
12 return 0;
13 }
Si al ejecutar el programa tecleamos un par de palabras, s´olo se muestra la primera:
una frase
La cadena le´ıda es una.
¿Qu´e ha ocurrido con los restantes caracteres tecleados? ¡Est´an a la espera de ser le´ıdos!
La siguiente cadena le´ıda, si hubiera un nuevo scanf , ser´ıa "frase". Si es lo que quer´ıamos,
perfecto, pero si no, el desastre puede ser may´usculo.
Introducci´on a la Programaci´on con C 95
2.2 Cadenas est´aticas
¿C´omo leer, pues, una frase completa? No hay forma sencilla de hacerlo con scanf . Tendre-
mos que recurrir a una funci´on diferente. La funci´on gets lee todos los caracteres que hay hasta
encontrar un salto de l´ınea. Dichos caracteres, excepto el salto de l´ınea, se almacenan a partir
de la direcci´on de memoria que se indique como argumento y se a˜nade un terminador.
Aqu´ı tienes un ejemplo:
1 #include <stdio.h>
2
3 #define MAXLON 11
4
5 int main(void)
6 {
7 char a[MAXLON+1], b[MAXLON+1];
8
9 printf ("Introduce una cadena: "); gets(a);
10 printf ("Introduce otra cadena: "); gets(b);
11 printf ("La primera es %s y la segunda es %sn", a, b);
12
13 return 0;
14 }
Ejecutemos el programa:
Introduce una cadena: uno dos
Introduce otra cadena: tres cuatro
La primera es uno dos y la segunda es tres cuatro
Overflow exploit
El manejo de cadenas C es complicado. . . y peligroso. La posibilidad de que se almace-
nen m´as caracteres de los que caben en una zona de memoria reservada para una cadena
ha dado lugar a una t´ecnica de cracking muy com´un: el overflow exploit (que significa
((aprovechamiento del desbordamiento))), tambi´en conocido por smash the stack (((machacar
la pila))).
Si un programa C lee una cadena con scanf o gets es vulnerable a este tipo de ataques.
La idea b´asica es la siguiente. Si c es una variable local a una funci´on (en el siguiente cap´ıtulo
veremos c´omo), reside en una zona de memoria especial: la pila. Podemos desbordar la zona
de memoria reservada para la cadena c escribiendo un texto m´as largo del que cabe en
ella. Cuando eso ocurre, estamos ocupando memoria en una zona de la pila que no nos
((pertenece)). Podemos conseguir as´ı escribir informaci´on en una zona de la pila reservada
a informaci´on como la direcci´on de retorno de la funci´on. El exploit se basa en asignar
a la direcci´on de retorno el valor de una direcci´on en la que habremos escrito una rutina
especial en c´odigo m´aquina. ¿Y c´omo conseguimos introducir una rutina en c´odigo m´aquina
en un programa ajeno? ¡En la propia cadena que provoca el desbordamiento, codific´andola
en binario! La rutina de c´odigo m´aquina suele ser sencilla: efect´ua una simple llamada al
sistema operativo para que ejecute un int´erprete de ´ordenes Unix. El int´erprete se ejecutar´a
con los mismos permisos que el programa que hemos reventado. Si el programa atacado
se ejecutaba con permisos de root, habremos conseguido ejecutar un int´erprete de ´ordenes
como root. ¡El ordenador es nuestro!
¿Y c´omo podemos proteger a nuestros programas de los overflow exploit? Pues, para
empezar, no utilizando nunca scanf o gets directamente. Como es posible leer de teclado
car´acter a car´acter (lo veremos en el cap´ıtulo dedicado a ficheros), podemos definir nuestra
propia funci´on de lectura de cadenas: una funci´on de lectura que controle que nunca se
escribe en una zona de memoria m´as informaci´on de la que cabe.
Dado que gets es tan vulnerable a los overflow exploit, el compilador de C te dar´a un
aviso cuando la uses. No te sorprendas, pues, cuando veas un mensaje como ´este: ((the
‘gets’ function is dangerous and should not be used)).
Lectura de cadenas y escalares: gets y sscanf
Y ahora, vamos con un problema al que te enfrentar´as en m´as de una ocasi´on: la lectura
alterna de cadenas y valores escalares. La mezcla de llamadas a scanf y a gets, produce efectos
96 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
curiosos que se derivan de la combinaci´on de su diferente comportamiento frente a los blancos.
El resultado suele ser una lectura incorrecta de los datos o incluso el bloqueo de la ejecuci´on
del programa. Los detalles son bastante escabrosos. Si tienes curiosidad, te los mostramos en el
apartado B.3.
Presentaremos en este cap´ıtulo una soluci´on directa que deber´as aplicar siempre que tu
programa alterne la lectura de cadenas con blancos y valores escalares (algo muy frecuente). La
soluci´on consiste en:
Si vas a leer una cadena usar gets.
Y si vas a leer un valor escalar, proceder en dos pasos:
• leer una l´ınea completa con gets (usa una variable auxiliar para ello),
• y extraer de ella los valores escalares que se deseaba leer con ayuda de la funci´on
sscanf .
La funci´on sscanf es similar a scanf (f´ıjate en la ((s)) inicial), pero no obtiene informaci´on
ley´endola del teclado, sino que la extrae de una cadena.
Un ejemplo ayudar´a a entender el procedimiento:
lecturas.c lecturas.c
1 #include <stdio.h>
2
3 #define MAXLINEA 80
4 #define MAXFRASE 40
5
6 int main(void)
7 {
8 int a, b;
9 char frase[MAXFRASE+1];
10 char linea[MAXLINEA+1];
11
12 printf ("Dame el valor de un entero:");
13 gets(linea); sscanf (linea, "%d", &a);
14
15 printf ("Introduce ahora una frase:");
16 gets(frase);
17
18 printf ("Y ahora, dame el valor de otro entero:");
19 gets(linea); sscanf (linea, "%d", &b);
20
21 printf ("Enteros le´ıdos: %d, %d.n", a, b);
22 printf ("Frase le´ıda: %s.n", frase);
23
24 return 0;
25 }
En el programa hemos definido una variable auxiliar, linea, que es una cadena con capacidad
para 80 caracteres m´as el terminador (puede resultar conveniente reservar m´as memoria para
ella en seg´un qu´e aplicaci´on). Cada vez que deseamos leer un valor escalar, leemos en linea un
texto que introduce el usuario y obtenemos el valor escalar con la funci´on sscanf . Dicha funci´on
recibe, como primer argumento, la cadena en linea; como segundo, una cadena con marcas
de formato; y como tercer par´ametro, la direcci´on de la variable escalar en la que queremos
depositar el resultado de la lectura.
Es un proceso un tanto inc´omodo, pero al que tenemos que acostumbrarnos. . . de momento.
2.2.4. Asignaci´on y copia de cadenas
Este programa, que pretende copiar una cadena en otra, parece correcto, pero no lo es:
1 #define MAXLON 10
2
3 int main(void)
Introducci´on a la Programaci´on con C 97
2.2 Cadenas est´aticas
4 {
5 char original[MAXLON+1] = "cadena";
6 char copia[MAXLON+1];
7
8 copia = original;
9
10 return 0;
11 }
Si compilas el programa, obtendr´as un error que te impedir´a obtener un ejecutable. Recuerda:
los identificadores de vectores est´aticos se consideran punteros inmutables y, a fin de cuentas,
las cadenas son vectores est´aticos (m´as adelante aprenderemos a usar vectores din´amicos). Para
efectuar una copia de una cadena, has de hacerlo car´acter a car´acter.
1 #define MAXLON 10
2
3 int main(void)
4 {
5 char original[MAXLON+1] = "cadena";
6 char copia[MAXLON+1];
7 int i;
8
9 for (i = 0; i <= MAXLON; i++)
10 copia[i] = original[i];
11
12 return 0;
13 }
F´ıjate en que el bucle recorre los 10 caracteres que realmente hay en original pero, de hecho,
s´olo necesitas copiar los caracteres que hay hasta el terminador, incluy´endole a ´el.
1 #define MAXLON 10
2
3 int main(void)
4 {
5 char original[MAXLON+1] = "cadena";
6 char copia[MAXLON+1];
7 int i;
8
9 for (i = 0; i <= MAXLON; i++) {
10 copia[i] = original[i];
11 if (copia[i] == ’0’)
12 break;
13 }
14
15 return 0;
16 }
original c
0
a
1
d
2
e
3
n
4
a
5
0
6 7 8 9
copia c
0
a
1
d
2
e
3
n
4
a
5
0
6 7 8 9
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 100 ¿Qu´e problema presenta esta otra versi´on del mismo programa?
1 #define MAXLON 10
2
3 int main(void)
4 {
5 char original[MAXLON+1] = "cadena";
6 char copia[MAXLON+1];
7 int i;
8
98 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
9 for (i = 0; i <= MAXLON; i++) {
10 if (copia[i] == ’0’)
11 break;
12 else
13 copia[i] = original[i];
14 }
15
16 return 0;
17 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
A´un podemos hacerlo ((mejor)):
1 #define MAXLON 10
2
3 int main(void)
4 {
5 char original[MAXLON+1] = "cadena";
6 char copia[MAXLON+1];
7 int i;
8
9 for (i = 0; original[i] != ’0’; i++) {
10 copia[i] = original[i];
11 copia[i] = ’0’;
12
13 return 0;
14 }
¿Ves? La condici´on del for controla si hemos llegado al terminador o no. Como el termina-
dor no llega a copiarse, lo a˜nadimos tan pronto finaliza el bucle. Este tipo de bucles, aunque
perfectamente legales, pueden resultar desconcertantes.
Una versi´on m´as del copiado de cadenas
Considera esta otra versi´on del copiado de cadenas:
1 #define MAXLON 10
2
3 int main(void)
4 {
5 char original[MAXLON+1] = "cadena";
6 char copia[MAXLON+1];
7 int i;
8
9 i = 0;
10 while ( (copia[i] = original[i++]) != ’0’) ;
11
12 return 0;
13 }
El bucle est´a vac´ıo y la condici´on del bucle while es un tanto extra˜na. Se aprovecha de
que la asignaci´on es una operaci´on que devuelve un valor, as´ı que lo puede comparar con el
terminador. Y no s´olo eso: el avance de i se logra con un postincremento en el mism´ısimo
acceso al elemento de original. Este tipo de retru´ecanos es muy habitual en los programas
C. Y es discutible que as´ı sea: los programas que hacen este tipo de cosas no tienen por
qu´e ser m´as r´apidos y resultan m´as dif´ıciles de entender (a menos que lleves mucho tiempo
programando en C).
Aqu´ı tienes una versi´on con una condici´on del bucle while diferente:
i = 0;
while (copia[i] = original[i++]) ;
¿Ves por qu´e funciona esta otra versi´on?
El copiado de cadenas es una acci´on frecuente, as´ı que hay funciones predefinidas para ello,
accesibles incluyendo la cabecera string.h:
Introducci´on a la Programaci´on con C 99
2.2 Cadenas est´aticas
1 #include <string.h>
2
3 #define MAXLON 10
4
5 int main(void)
6 {
7 char original[MAXLON+1] = "cadena";
8 char copia[MAXLON+1];
9
10 strcpy(copia, original); // Copia el contenido de original en copia.
11
12 return 0;
13 }
Ten cuidado: strcpy (abreviatura de ((string copy))) no comprueba si el destino de la copia tiene
capacidad suficiente para la cadena, as´ı que puede provocar un desbordamiento. La funci´on
strcpy se limita a copiar car´acter a car´acter hasta llegar a un car´acter nulo.
Copias (m´as) seguras
Hemos dicho que strcpy presenta un fallo de seguridad: no comprueba si el destino es capaz
de albergar todos los caracteres de la cadena original. Si quieres asegurarte de no rebasar
la capacidad del vector destino puedes usar strncpy, una versi´on de strcpy que copia la
cadena, pero con un l´ımite al n´umero m´aximo de caracteres:
1 #include <string.h>
2
3 #define MAXLON 10
4
5 int main(void)
6 {
7 char original[MAXLON+1] = "cadena";
8 char copia[MAXLON+1];
9
10 strncpy(copia, original, MAXLON+1); // Copia, a lo sumo, MAXLON+1 caracteres.
11
12 return 0;
13 }
Pero tampoco strncpy es perfecta. Si la cadena original tiene m´as caracteres de los que
puede almacenar la cadena destino, la copia es imperfecta: no acabar´a en ’0’. De todos
modos, puedes encargarte t´u mismo de terminar la cadena en el ´ultimo car´acter, por si
acaso:
1 #include <string.h>
2
3 #define MAXLON 10
4
5 int main(void)
6 {
7 char original[MAXLON+1] = "cadena";
8 char copia[MAXLON+1];
9
10 strncpy(copia, original, MAXLON+1);
11 copia[MAXLON] = ’0’;
12
13 return 0;
14 }
Tampoco est´a permitido asignar un literal de cadena a un vector de caracteres fuera de la
zona de declaraci´on de variables. Es decir, este programa es incorrecto:
1 #define MAXLON 10
100 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
2
3 int main(void)
4 {
5 char a[MAXLON+1];
6
7 a = "cadena"; //
!
Mal!
8
9 return 0;
10 }
Si deseas asignar un literal de cadena, tendr´as que hacerlo con la ayuda de strcpy:
1 #include <string.h>
2
3 #define MAXLON 10
4
5 int main(void)
6 {
7 char a[MAXLON+1];
8
9 strcpy(a, "cadena");
10
11 return 0;
12 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 101 Dise˜na un programa que lea una cadena y copie en otra una versi´on encriptada. La
encriptaci´on convertir´a cada letra (del alfabeto ingl´es) en la que le sigue en la tabla ASCII
(excepto en el caso de las letras ((z)) y ((Z)), que ser´an sustituidas por ((a)) y ((A)), respectivamente.)
No uses la funci´on strcpy.
· 102 Dise˜na un programa que lea una cadena que posiblemente contenga letras may´usculas
y copie en otra una versi´on de la misma cuyas letras sean todas min´usculas. No uses la funci´on
strcpy.
· 103 Dise˜na un programa que lea una cadena que posiblemente contenga letras may´usculas
y copie en otra una versi´on de la misma cuyas letras sean todas min´usculas. Usa la funci´on
strcpy para obtener un duplicado de la cadena y, despu´es, recorre la copia para ir sustituyendo
en ella las letras may´usculas por sus correspondientes min´usculas.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.5. Longitud de una cadena
El convenio de terminar una cadena con el car´acter nulo permite conocer f´acilmente la longitud
de una cadena:
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char a[MAXLON+1];
8 int i;
9
10 printf ("Introduce una cadena (m´ax. %d cars.): ", MAXLON);
11 gets(a);
12 i = 0;
13 while (a[i] != ’0’)
14 i++;
15 printf ("Longitud de la cadena: %dn", i);
16
17 return 0;
18 }
Introducci´on a la Programaci´on con C 101
2.2 Cadenas est´aticas
El estilo C
El programa que hemos presentado para calcular la longitud de una cadena es un programa
C correcto, pero no es as´ı como un programador C expresar´ıa esa misma idea. ¡No hace
falta que el bucle incluya sentencia alguna!:
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char a[MAXLON+1];
8 int i;
9
10 printf ("Introduce una cadena (m´ax. %d cars.): ", MAXLON);
11 gets(a);
12 i = 0;
13 while (a[i++] != ’0’) ; // Observa que no hay sentencia alguna en el while.
14 printf ("Longitud de la cadena: %dn", i-1);
15
16 return 0;
17 }
El operador de postincremento permite aumentar en uno el valor de i justo despu´es de
consultar el valor de a[i]. Eso s´ı, hemos tenido que modificar el valor mostrado como
longitud, pues ahora i acaba valiendo uno m´as.
Es m´as, ni siquiera es necesario efectuar comparaci´on alguna. El bucle se puede sustituir
por este otro:
i = 0;
while (a[i++]) ;
El bucle funciona correctamente porque el valor ’0’ significa ((falso)) cuando se interpreta
como valor l´ogico. El bucle itera, pues, hasta llegar a un valor falso, es decir, a un terminador.
Algunos problemas con el operador de autoincremento
¿Qu´e esperamos que resulte de ejecutar esta sentencia?
1 int a[5] = {0, 0, 0, 0, 0};
2
3 i = 1;
4 a[i] = i++;
Hay dos posibles interpretaciones:
Se eval´ua primero la parte derecha de la asignaci´on, as´ı que i pasa a valer 2 y se
asigna ese valor en a[2].
Se eval´ua primero la asignaci´on, con lo que se asigna el valor 1 en a[1] y, despu´es,
se incrementa el valor de i, que pasa a valer 2.
¿Qu´e hace C? No se sabe. La especificaci´on del lenguaje est´andar indica que el resultado
est´a indefinido. Cada compilador elige qu´e hacer, as´ı que ese tipo de sentencias pueden dar
problemas de portabilidad. Conviene, pues, evitarlas.
Calcular la longitud de una cadena es una operaci´on frecuentemente utilizada, as´ı que est´a
predefinida en la biblioteca de tratamiento de cadenas. Si inclu´ımos la cabecera string.h,
podemos usar la funci´on strlen (abreviatura de ((string length))):
1 #include <stdio.h>
2 #include <string.h>
3
102 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
while o for
Los bucles while pueden sustituirse muchas veces por bucles for equivalentes, bastante
m´as compactos:
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char a[MAXLON+1];
8 int i;
9
10 printf ("Introduce una cadena (m´ax. %d cars.): ", MAXLON);
11 gets(a);
12 for (i=0; a[i] != ’0’; i++) ; // Tampoco hay sentencia alguna en el for.
13 printf ("Longitud de la cadena: %dn", i);
14
15 return 0;
16 }
Tambi´en aqu´ı es superflua la comparaci´on:
for (i=0; a[i]; i++) ;
Todas las versiones del programa que hemos presentado son equivalentes. Escoger una
u otra es cuesti´on de estilo.
4 #define MAXLON 80
5
6 int main(void)
7 {
8 char a[MAXLON+1];
9 int l;
10
11 printf ("Introduce una cadena (m´ax. %d cars.): ", MAXLON);
12 gets(a);
13 l = strlen(a);
14 printf ("Longitud de la cadena: %dn", l);
15
16 return 0;
17 }
Has de ser consciente de qu´e hace strlen: lo mismo que hac´ıa el primer programa, es decir,
recorrer la cadena de izquierda a derecha incrementando un contador hasta llegar al terminador
nulo. Esto implica que tarde tanto m´as cuanto m´as larga sea la cadena. Has de estar al tanto,
pues, de la fuente de ineficiencia que puede suponer utilizar directamente strlen en lugares
cr´ıticos como los bucles. Por ejemplo, esta funci´on cuenta las vocales min´usculas de una cadena
le´ıda por teclado:
1 #include <stdio.h>
2 #include <string.h>
3
4 #define MAXLON 80
5
6 int main(void)
7 {
8 char a[MAXLON+1];
9 int i, contador;
10
11 printf ("Introduce una cadena (m´ax. %d cars.): ", MAXLON);
12 gets(a);
13 contador = 0;
Introducci´on a la Programaci´on con C 103
2.2 Cadenas est´aticas
14 for (i = 0; i < strlen(a); i++)
15 if (a[i] == ’a’ || a[i] == ’e’ || a[i] == ’i’ || a[i] == ’o’ || a[i] == ’u’)
16 contador++;
17 printf ("Vocales min´usculas: %dn", contador);
18
19 return 0;
20 }
Pero tiene un problema de eficiencia. Con cada iteraci´on del bucle for se llama a strlen y strlen
tarda un tiempo proporcional a la longitud de la cadena. Si la cadena tiene, pongamos, 60
caracteres, se llamar´a a strlen 60 veces para efectuar la comparaci´on, y para cada llamada,
strlen tardar´a unos 60 pasos en devolver lo mismo: el valor 60. Esta nueva versi´on del mismo
programa no presenta ese inconveniente:
1 #include <stdio.h>
2 #include <string.h>
3
4 #define MAXLON 80
5
6 int main(void)
7 {
8 char a[MAXLON+1];
9 int i, longitud , contador;
10
11 printf ("Introduce una cadena (m´ax. %d cars.): ", MAXLON);
12 gets(a);
13 longitud = strlen(cadena);
14 contador = 0;
15 for (i = 0; i < longitud ; i++)
16 if (a[i] == ’a’ || a[i] == ’e’ || a[i] == ’i’ || a[i] == ’o’ || a[i] == ’u’)
17 contador++;
18 printf ("Vocales min´usculas: %dn", contador);
19
20 return 0;
21 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 104 Dise˜na un programa que lea una cadena y la invierta.
· 105 Dise˜na un programa que lea una palabra y determine si es o no es pal´ındromo.
· 106 Dise˜na un programa que lea una frase y determine si es o no es pal´ındromo. Recuerda
que los espacios en blanco y los signos de puntuaci´on no se deben tener en cuenta a la hora de
determinar si la frase es pal´ındromo.
· 107 Escribe un programa C que lea dos cadenas y muestre el ´ındice del car´acter de la
primera cadena en el que empieza, por primera vez, la segunda cadena. Si la segunda cadena
no est´a contenida en la primera, el programa nos lo har´a saber.
(Ejemplo: si la primera cadena es "un ejercicio de ejemplo" y la segunda es "eje", el
programa mostrar´a el valor 3.)
· 108 Escribe un programa C que lea dos cadenas y muestre el ´ındice del car´acter de la
primera cadena en el que empieza por ´ultima vez una aparici´on de la segunda cadena. Si la
segunda cadena no est´a contenida en la primera, el programa nos lo har´a saber.
(Ejemplo: si la primera cadena es "un ejercicio de ejemplo" y la segunda es "eje", el
programa mostrar´a el valor 16.)
· 109 Escribe un programa que lea una l´ınea y haga una copia de ella eliminando los espacios
en blanco que haya al principio y al final de la misma.
· 110 Escribe un programa que lea repetidamente l´ıneas con el nombre completo de una
persona. Para cada persona, guardar´a temporalmente en una cadena sus iniciales (las letras
con may´usculas) separadas por puntos y espacios en blanco y mostrar´a el resultado en pantalla.
El programa finalizar´a cuando el usuario escriba una l´ınea en blanco.
104 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
· 111 Dise˜na un programa C que lea un entero n y una cadena a y muestre por pantalla el
valor (en base 10) de la cadena a si se interpreta como un n´umero en base n. El valor de n debe
estar comprendido entre 2 y 16. Si la cadena a contiene un car´acter que no corresponde a un
d´ıgito en base n, notificar´a el error y no efectuar´a c´alculo alguno.
Ejemplos:
si a es "ff" y n es 16, se mostrar´a el valor 255;
si a es "f0" y n es 15, se notificar´a un error: ((f no es un d´ıgito en base 15));
si a es "1111" y n es 2, se mostrar´a el valor 15.
· 112 Dise˜na un programa C que lea una l´ınea y muestre por pantalla el n´umero de palabras
que hay en ella.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.6. Concatenaci´on
Python permit´ıa concatenar cadenas con el operador +. En C no puedes usar + para concatenar
cadenas. Una posibilidad es que las concatenes t´u mismo ((a mano)), con bucles. Este programa,
por ejemplo, pide dos cadenas y concatena la segunda a la primera:
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char a[MAXLON+1], b[MAXLON+1];
8 int longa, longb;
9 int i;
10
11 printf ("Introduce un texto (m´ax. %d cars.): ", MAXLON); gets(a);
12 printf ("Introduce otro texto (m´ax. %d cars.): ", MAXLON); gets(b);
13
14 longa = strlen(a);
15 longb = strlen(b);
16 for (i=0; i<longb; i++)
17 a[longa+i] = b[i];
18 a[longa+longb] = ’0’;
19 printf ("Concatenaci´on de ambos: %s", a);
20
21 return 0;
22 }
Pero es mejor usar la funci´on de librer´ıa strcat (por ((string concatenate))):
1 #include <stdio.h>
2 #include <string.h>
3
4 #define MAXLON 80
5
6 int main(void)
7 {
8 char a[MAXLON+1], b[MAXLON+1];
9
10 printf ("Introduce un texto (m´ax. %d cars.): ", MAXLON);
11 gets(a);
12 printf ("Introduce otro texto (m´ax. %d cars.): ", MAXLON);
13 gets(b);
14 strcat(a, b); // Equivale a la asignaci´on Python a = a + b
15 printf ("Concatenaci´on de ambos: %s", a);
16
17 return 0;
18 }
Introducci´on a la Programaci´on con C 105
2.2 Cadenas est´aticas
Si quieres dejar el resultado de la concatenaci´on en una variable distinta, deber´as actuar en
dos pasos:
1 #include <stdio.h>
2 #include <string.h>
3
4 #define MAXLON 80
5
6 int main(void)
7 {
8 char a[MAXLON+1], b[MAXLON+1], c[MAXLON+1];
9
10 printf ("Introduce un texto (m´ax. %d cars.): ", MAXLON);
11 gets(a);
12 printf ("Introduce otro texto (m´ax. %d cars.): ", MAXLON);
13 gets(b);
14 strcpy(c, a); // ´Esta seguida de...
15 strcat(c, b); // ... ´esta equivale a la sentencia Python c = a + b
16 printf ("Concatenaci´on de ambos: %s", c);
17
18 return 0;
19 }
Recuerda que es responsabilidad del programador asegurarse de que la cadena que recibe la
concatenaci´on dispone de capacidad suficiente para almacenar la cadena resultante.
Por cierto, el operador de repetici´on de cadenas que encontr´abamos en Python (operador
*) no est´a disponible en C ni hay funci´on predefinida que lo proporcione.
Un car´acter no es una cadena
Un error frecuente es intentar a˜nadir un car´acter a una cadena con strcat o asign´arselo como
´unico car´acter con strcpy:
char linea[10] = "cadena";
char caracter = ’s’;
strcat(linea, caracter); //
!
Mal!
strcpy(linea, ’x’); //
!
Mal!
Recuerda: los dos datos de strcat y strcpy han de ser cadenas y no es aceptable que uno
de ellos sea un car´acter.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 113 Escribe un programa C que lea el nombre y los dos apellidos de una persona en tres
cadenas. A continuaci´on, el programa formar´a una s´ola cadena en la que aparezcan el nombre
y los apellidos separados por espacios en blanco.
· 114 Escribe un programa C que lea un verbo regular de la primera conjugaci´on y lo mues-
tre por pantalla conjugado en presente de indicativo. Por ejemplo, si lee el texto programar,
mostrar´a por pantalla:
yo programo
t´u programas
´el programa
nosotros programamos
vosotros program´ais
ellos programan
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.7. Comparaci´on de cadenas
Tampoco los operadores de comparaci´on (==, !=, <, <=, >, >=) funcionan con cadenas. Existe, no
obstante, una funci´on de string.h que permite paliar esta carencia de C: strcmp (abreviatura
106 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
de ((string comparison))). La funci´on strcmp recibe dos cadenas, a y b, y devuelve un entero. El
entero que resulta de efectuar la llamada strcmp(a, b) codifica el resultado de la comparaci´on:
es menor que cero si la cadena a es menor que b,
es 0 si la cadena a es igual que b, y
es mayor que cero si la cadena a es mayor que b.
Naturalmente, menor significa que va delante en orden alfab´etico, y mayor que va detr´as.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 115 Dise˜na un programa C que lea dos cadenas y, si la primera es menor o igual que la
segunda, imprima el texto ((menor o igual)).
· 116 ¿Qu´e valor devolver´a la llamada strcmp("21", "112")?
· 117 Escribe un programa que lea dos cadenas, a y b (con capacidad para 80 caracteres), y
muestre por pantalla −1 si a es menor que b, 0 si a es igual que b, y 1 si a es mayor que b. Est´a
prohibido que utilices la funci´on strcmp.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.8. Funciones ´utiles para manejar caracteres
No s´olo string.h contiene funciones ´utiles para el tratamiento de cadenas. En ctype.h en-
contrar´as unas funciones que permiten hacer c´omodamente preguntas acerca de los caracteres,
como si son may´usculas, min´usculas, d´ıgitos, etc:
isalnum(car´acter): devuelve cierto (un entero cualquiera distinto de cero) si car´acter es
una letra o d´ıgito, y falso (el valor entero 0) en caso contrario,
isalpha(car´acter): devuelve cierto si car´acter es una letra, y falso en caso contrario,
isblank(car´acter): devuelve cierto si car´acter es un espacio en blanco o un tabulador,
isdigit(car´acter) devuelve cierto si car´acter es un d´ıgito, y falso en caso contrario,
isspace(car´acter): devuelve cierto si car´acter es un espacio en blanco, un salto de l´ınea,
un retorno de carro, un tabulador, etc., y falso en caso contrario,
islower(car´acter): devuelve cierto si car´acter es una letra min´uscula, y falso en caso
contrario,
isupper(car´acter): devuelve cierto si car´acter es una letra may´uscula, y falso en caso
contrario.
Tambi´en en ctype.h encontrar´as un par de funciones ´utiles para convertir caracteres de min´uscula
a may´uscula y viceversa:
toupper(car´acter): devuelve la may´uscula asociada a car´acter, si la tiene; si no, devuelve
el mismo car´acter,
tolower(car´acter): devuelve la min´uscula asociada a car´acter, si la tiene; si no, devuelve
el mismo car´acter.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 118 ¿Qu´e problema presenta este programa?
1 #include <stdio.h>
2 #include <ctype.h>
3
4 int main(void)
5 {
6 char b[2] = "a";
7
8 if (isalpha(b))
Introducci´on a la Programaci´on con C 107
2.2 Cadenas est´aticas
9 printf ("Es una letran");
10 else
11 printf ("No es una letran");
12
13 return 0;
14 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.9. Escritura en cadenas: sprintf
Hay una funci´on que puede simplificar notablemente la creaci´on de cadenas cuyo contenido se
debe calcular a partir de uno o m´as valores: sprintf , disponible incluyendo la cabecera stdio.h
(se trata, en cierto modo, de la operaci´on complementaria de sscanf ). La funci´on sprintf se
comporta como printf , salvo por un ((detalle)): no escribe texto en pantalla, sino que lo almacena
en una cadena.
F´ıjate en este ejemplo:
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char a[MAXLON+1] = "una";
8 char b[MAXLON+1] = "cadena";
9 char c[MAXLON+1];
10
11 sprintf (c, "%s %s", a, b);
12 printf ("%sn", c);
13
14 return 0;
15 }
Si ejecutas el programa aparecer´a lo siguiente en pantalla:
una cadena
Como puedes ver, se ha asignado a c el valor de a seguido de un espacio en blanco y
de la cadena b. Podr´ıamos haber conseguido el mismo efecto con llamadas a strcpy(c, a),
strcat(c, " ") y strcat(c, b), pero sprintf resulta m´as legible y no cuesta mucho aprender a
usarla, pues ya sabemos usar printf . No olvides que t´u eres responsable de que la informaci´on
que se almacena en c quepa.
En Python hay una acci´on an´aloga al sprintf de C: la asignaci´on a una variable de una
cadena formada con el operador de formato. El mismo programa se podr´ıa haber escrito en
Python as´ı:
1 # Ojo: programa Python
2 a = ’una’
3 b = ’cadena’
4 c = ’%s %s’ % (a, b) # Operaci´on an´aloga a sprintf en C.
5 print c
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 119 ¿Qu´e almacena en la cadena a la siguiente sentencia?
sprintf (a, "%d-%c-%d %s", 1, 48, 2, "si");
· 120 Escribe un programa que pida el nombre y los dos apellidos de una persona. Cada uno
de esos tres datos debe almacenarse en una variable independiente. A continuaci´on, el programa
crear´a y mostrar´a una nueva cadena con los dos apellidos y el nombre (separado de los apellidos
por una coma). Por ejemplo, Juan P´erez L´opez dar´a lugar a la cadena "P´erez L´opez, Juan".
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
108 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
2.2.10. Un programa de ejemplo
Vamos a implementar un programa que lee por teclado una l´ınea de texto y muestra por
pantalla una cadena en la que las secuencias de blancos de la cadena original (espacios en
blanco, tabuladores, etc.) se han sustituido por un s´olo espacio en blanco. Si, por ejemplo,
el programa lee la cadena "una cadena con blancos ", mostrar´a por pantalla la
cadena ((normalizada)) "una cadena con blancos ".
normaliza.c normaliza.c
1 #include <stdio.h>
2 #include <string.h>
3 #include <ctype.h>
4
5 #define MAXLON 80
6
7 int main(void)
8 {
9 char a[MAXLON+1], b[MAXLON+1];
10 int longitud, i, j;
11
12 printf ("Introduce una cadena (m´ax. %d cars.): ", MAXLON);
13 gets(a);
14 longitud = strlen(a);
15 b[0] = a[0];
16 j = 1;
17 for (i=1; i<longitud; i++)
18 if (!isspace(a[i]) || (isspace(a[i]) && !isspace(a[i-1])))
19 b[j++] = a[i];
20 b[j] = ’0’;
21 printf ("La cadena normalizada es %sn", b);
22
23 return 0;
24 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 121 Modifica normaliza.c para que elimine, si los hay, los blancos inicial y final de la
cadena normalizada.
· 122 Haz un programa que lea una frase y construya una cadena que s´olo contenga sus letras
min´usculas o may´usculas en el mismo orden con que aparecen en la frase.
· 123 Haz un programa que lea una frase y construya una cadena que s´olo contenga sus
letras min´usculas o may´usculas en el mismo orden con que aparecen en la frase, pero sin repetir
ninguna.
· 124 Lee un texto por teclado (con un m´aximo de 1000 caracteres) y muestra por pantalla
la frecuencia de aparici´on de cada una de las letras del alfabeto (considera ´unicamente letras
del alfabeto ingl´es), sin distinguir entre letras may´usculas y min´usculas (una aparici´on de la
letra e y otra de la letra E cuentan como dos ocurrencias de la letra e).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3. Vectores multidimensionales
Podemos declarar vectores de m´as de una dimensi´on muy f´acilmente:
int a[10][5];
float b[3][2][4];
En este ejemplo, a es una matriz de 10 × 5 enteros y b es un vector de tres dimensiones con
3 × 2 × 4 n´umeros en coma flotante.
Puedes acceder a un elemento cualquiera de los vectores a o b utilizando tantos ´ındices
como dimensiones tiene el vector: a[4][2] y b[1][0][3], por ejemplo, son elementos de a y b,
respectivamente.
Introducci´on a la Programaci´on con C 109
2.3 Vectores multidimensionales
La inicializaci´on de los vectores multidimensionales necesita tantos bucles anidados como
dimensiones tengan ´estos:
1 int main(void)
2 {
3 int a[10][5];
4 float b[3][2][4];
5 int i, j, k;
6
7 for (i=0; i<10; i++)
8 for (j=0; j<5; j++)
9 a[i][j] = 0;
10
11 for (i=0; i<3; i++)
12 for (j=0; j<2; j++)
13 for (k=0; k<4; k++)
14 b[i][j][k] = 0.0;
15
16 return 0;
17 }
Tambi´en puedes inicializar expl´ıcitamente un vector multidimensional:
int c[3][3] = { {1, 0, 0},
{0, 1, 0},
{0, 0, 1} };
2.3.1. Sobre la disposici´on de los vectores multidimensionales en memoria
Cuando el compilador de C detecta la declaraci´on de un vector multidimensional, reserva tantas
posiciones contiguas de memoria como sea preciso para albergar todas sus celdas.
Por ejemplo, ante la declaraci´on int a[3][3], C reserva 9 celdas de 4 bytes, es decir, 36
bytes. He aqu´ı c´omo se disponen las celdas en memoria, suponiendo que la zona de memoria
asignada empieza en la direcci´on 1000:
996:
a[0][0]1000:
a[0][1]1004:
a[0][2]1008:
a[1][0]1012:
a[1][1]1016:
a[1][2]1020:
a[2][0]1024:
a[2][1]1028:
a[2][2]1032:
1036:
Cuando accedemos a un elemento a[i][j], C sabe a qu´e celda de memoria acceder sumando
a la direcci´on de a el valor (i*3+j)*4 (el 4 es el tama˜no de un int y el 3 es el n´umero de
columnas).
Aun siendo conscientes de c´omo representa C la memoria, nosotros trabajaremos con una
representaci´on de una matriz de 3 × 3 como ´esta:
a
0 1 2
0
1
2
Como puedes ver, lo relevante es que a es asimilable a un puntero a la zona de memoria en la
que est´an dispuestos los elementos de la matriz.
110 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 125 Este programa es incorrecto. ¿Por qu´e? Aun siendo incorrecto, produce cierta salida
por pantalla. ¿Qu´e muestra?
matriz mal.c matriz mal.c
1 #include <stdio.h>
2
3 #define TALLA 3
4
5 int main(void)
6 {
7 int a[TALLA][TALLA];
8 int i, j;
9
10 for (i=0; i<TALLA; i++)
11 for (j=0; j<TALLA; j++)
12 a[i][j] = 10*i+j;
13
14 for (j=0; j<TALLA*TALLA; j++)
15 printf ("%dn", a[0][j]);
16
17 return 0;
18 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3.2. Un ejemplo: c´alculo matricial
Para ilustrar el manejo de vectores multidimensionales construiremos ahora un programa que
lee de teclado dos matrices de n´umeros en coma flotante y muestra por pantalla su suma y su
producto. Las matrices le´ıdas ser´an de 3 × 3 y se denominar´an a y b. El resultado de la suma
se almacenar´a en una matriz s y el del producto en otra p.
Aqu´ı tienes el programa completo:
matrices.c matrices.c
1 #include <stdio.h>
2
3 #define TALLA 3
4
5 int main(void)
6 {
7 float a[TALLA][TALLA], b[TALLA][TALLA];
8 float s[TALLA][TALLA], p[TALLA][TALLA];
9 int i, j, k;
10
11 /* Lectura de la matriz a */
12 for (i=0; i<TALLA; i++)
13 for (j=0; j<TALLA; j++) {
14 printf ("Elemento (%d, %d): ", i, j); scanf ("%f", &a[i][j]);
15 }
16
17 /* Lectura de la matriz b */
18 for (i=0; i<TALLA; i++)
19 for (j=0; j<TALLA; j++) {
20 printf ("Elemento (%d, %d): ", i, j); scanf ("%f", &b[i][j]);
21 }
22
23 /* C´alculo de la suma */
24 for (i=0; i<TALLA; i++)
25 for (j=0; j<TALLA; j++)
26 s[i][j] = a[i][j] + b[i][j];
27
28 /* C´alculo del producto */
Introducci´on a la Programaci´on con C 111
2.3 Vectores multidimensionales
29 for (i=0; i<TALLA; i++)
30 for (j=0; j<TALLA; j++) {
31 p[i][j] = 0.0;
32 for (k=0; k<TALLA; k++)
33 p[i][j] += a[i][k] * b[k][j];
34 }
35
36 /* Impresi´on del resultado de la suma */
37 printf ("Suman");
38 for (i=0; i<TALLA; i++) {
39 for (j=0; j<TALLA; j++)
40 printf ("%8.3f", s[i][j]);
41 printf ("n");
42 }
43
44 /* Impresi´on del resultado del producto */
45 printf ("Producton");
46 for (i=0; i<TALLA; i++) {
47 for (j=0; j<TALLA; j++)
48 printf ("%8.3f", p[i][j]);
49 printf ("n");
50 }
51
52 return 0;
53 }
A´un no sabemos definir nuestras propias funciones. En el pr´oximo cap´ıtulo volveremos a ver
este programa y lo modificaremos para que use funciones definidas por nosotros.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 126 En una estaci´on meteorol´ogica registramos la temperatura (en grados cent´ıgrados) cada
hora durante una semana. Almacenamos el resultado en una matriz de 7 × 24 (cada fila de la
matriz contiene las 24 mediciones de un d´ıa). Dise˜na un programa que lea los datos por teclado
y muestre:
La m´axima y m´ınima temperaturas de la semana.
La m´axima y m´ınima temperaturas de cada d´ıa.
La temperatura media de la semana.
La temperatura media de cada d´ıa.
El n´umero de d´ıas en los que la temperatura media fue superior a 30 grados.
· 127 Representamos diez ciudades con n´umeros del 0 al 9. Cuando hay carretera que une
directamente a dos ciudades i y j, almacenamos su distancia en kil´ometros en la celda d[i][j]
de una matriz de 10×10 enteros. Si no hay carretera entre ambas ciudades, el valor almacenado
en su celda de d es cero. Nos suministran un vector en el que se describe un trayecto que pasa
por las 10 ciudades. Determina si se trata de un trayecto v´alido (las dos ciudades de todo
par consecutivo est´an unidas por un tramo de carretera) y, en tal caso, devuelve el n´umero de
kil´ometros del trayecto. Si el trayecto no es v´alido, ind´ıcalo con un mensaje por pantalla.
La matriz de distancias deber´as inicializarla expl´ıcitamente al declararla. El vector con el
recorrido de ciudades deber´as leerlo de teclado.
· 128 Dise˜na un programa que lea los elementos de una matriz de 4 × 5 flotantes y genere
un vector de talla 4 en el que cada elemento contenga el sumatorio de los elementos de cada
fila. El programa debe mostrar la matriz original y el vector en este formato (evidentemente,
los valores deben ser los que correspondan a lo introducido por el usuario):
0 1 2 3 4 Suma
0 [ +27.33 +22.22 +10.00 +0.00 -22.22] -> +37.33
1 [ +5.00 +0.00 -1.50 +2.50 +10.00] -> +16.00
2 [ +3.45 +2.33 -4.56 +12.56 +12.01] -> +25.79
3 [ +1.02 +2.22 +12.70 +34.00 +12.00] -> +61.94
112 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
El programa que hemos presentado adolece de un serio inconveniente si nuestro objetivo era
construir un programa ((general)) para multiplicar matrices: s´olo puede trabajar con matrices de
TALLA×TALLA, o sea, de 3×3. ¿Y si quisi´eramos trabajar con matrices de tama˜nos arbitrarios?
El primer problema al que nos enfrentar´ıamos es el de que las matrices han de tener una talla
m´axima: no podemos, con lo que sabemos por ahora, reservar un espacio de memoria para las
matrices que dependa de datos que nos suministra el usuario en tiempo de ejecuci´on. Usaremos,
pues, una constante MAXTALLA con un valor razonablemente grande: pongamos 10. Ello permitir´a
trabajar con matrices con un n´umero de filas y columnas menor o igual que 10, aunque ser´a a
costa de malgastar memoria.
matrices.c
1 #include <stdio.h>
2
3 #define MAXTALLA 10
4
5 int main(void)
6 {
7 float a[MAXTALLA][MAXTALLA], b[MAXTALLA][MAXTALLA];
8 float s[MAXTALLA][MAXTALLA], p[MAXTALLA][MAXTALLA];
9 ...
El n´umero de filas y columnas de a se pedir´a al usuario y se almacenar´a en sendas variables:
filas_a y columnas_a. Este gr´afico ilustra su papel: la matriz a es de 10 × 10, pero s´olo usamos
una parte de ella (la zona sombreada) y podemos determinar qu´e zona es porque filas_a y
columnas_a nos se˜nalan hasta qu´e fila y columna llega la zona ´util:
3columnas a
a
0 1 2 3 4 5 6 7 8 9
0
1
2
3
4
5
6
7
8
9
5filas a
Lo mismo se aplicar´a al n´umero de filas y columnas de b. Te mostramos el programa hasta el
punto en que leemos la matriz a:
matrices.c
1 #include <stdio.h>
2
3 #define MAXTALLA 10
4
5 int main(void)
6 {
7 float a[MAXTALLA][MAXTALLA], b[MAXTALLA][MAXTALLA];
8 float s[MAXTALLA][MAXTALLA], p[MAXTALLA][MAXTALLA];
9 int filas_a, columnas_a, filas_b, columnas_b ;
10 int i, j, k;
11
12 /* Lectura de la matriz a */
13 printf ("Filas de a : "); scanf ("%d", &filas_a );
Introducci´on a la Programaci´on con C 113
2.3 Vectores multidimensionales
14 printf ("Columnas de a: "); scanf ("%d", &columnas_a );
15
16 for (i=0; i<filas_a ; i++)
17 for (j=0; j<columnas_a ; j++) {
18 printf ("Elemento (%d, %d): ", i, j); scanf ("%f", &a[i][j]);
19 }
20 ...
(Enc´argate t´u mismo de la lectura de b.)
La suma s´olo es factible si filas a es igual a filas b y columnas a es igual a columnas b.
matrices.c
1 #include <stdio.h>
2
3 #define MAXTALLA 10
4
5 int main(void)
6 {
7 float a[MAXTALLA][MAXTALLA], b[MAXTALLA][MAXTALLA];
8 float s[MAXTALLA][MAXTALLA], p[MAXTALLA][MAXTALLA];
9 int filas_a, columnas_a, filas_b, columnas_b;
10 int filas_s, columnas_s ;
11 int i, j, k;
12
13 /* Lectura de la matriz a */
14 printf ("Filas de a : "); scanf ("%d", &filas_a);
15 printf ("Columnas de a: "); scanf ("%d", &columnas_a);
16 for (i=0; i<filas_a; i++)
17 for (j=0; j<columnas_a; j++) {
18 printf ("Elemento (%d, %d): ", i, j); scanf ("%f", &a[i][j]);
19 }
20
21 /* Lectura de la matriz b */
22 ...
23
24 /* C´alculo de la suma */
25 if (filas_a == filas_b && columnas_a == columnas_b) {
26 filas_s = filas_a;
27 columnas_s = columnas_a;
28 for (i=0; i<filas_s; i++)
29 for (j=0; j<filas_s; j++)
30 s[i][j] = a[i][j] + b[i][j];
31 }
32
33 /* Impresi´on del resultado de la suma */
34 if (filas_a == filas_b && columnas_a == columnas_b) {
35 printf ("Suman");
36 for (i=0; i<filas_s; i++) {
37 for (j=0; j<columnas_s; j++)
38 printf ("%8.3f", s[i][j]);
39 printf ("n");
40 }
41 }
42 else
43 printf ("Matrices no compatibles para la suma.n");
44
45 ...
Recuerda que una matriz de n × m elementos se puede multiplicar por otra de n × m
elementos s´olo si m es igual a n (o sea, el n´umero de columnas de la primera es igual al de filas
de la segunda) y que la matriz resultante es de dimensi´on n × m .
matrices 1.c matrices.c
1 #include <stdio.h>
114 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
2
3 #define MAXTALLA 10
4
5 int main(void)
6 {
7 float a[MAXTALLA][MAXTALLA], b[MAXTALLA][MAXTALLA];
8 float s[MAXTALLA][MAXTALLA], p[MAXTALLA][MAXTALLA];
9 int filas_a, columnas_a, filas_b, columnas_b;
10 int filas_s, columnas_s, filas_p, columnas_p ;
11 int i, j, k;
12
13 /* Lectura de la matriz a */
14 printf ("Filas de a : "); scanf ("%d", &filas_a);
15 printf ("Columnas de a: "); scanf ("%d", &columnas_a);
16 for (i=0; i<filas_a; i++)
17 for (j=0; j<columnas_a; j++) {
18 printf ("Elemento (%d, %d): ", i, j); scanf ("%f", &a[i][j]);
19 }
20
21 /* Lectura de la matriz b */
22 printf ("Filas de b : "); scanf ("%d", &filas_b);
23 printf ("Columnas de b: "); scanf ("%d", &columnas_b);
24 for (i=0; i<filas_b; i++)
25 for (j=0; j<columnas_b; j++) {
26 printf ("Elemento (%d, %d): ", i, j); scanf ("%f", &b[i][j]);
27 }
28
29 /* C´alculo de la suma */
30 if (filas_a == filas_b && columnas_a == columnas_b) {
31 filas_s = filas_a;
32 columnas_s = columnas_a;
33 for (i=0; i<filas_s; i++)
34 for (j=0; j<filas_s; j++)
35 s[i][j] = a[i][j] + b[i][j];
36 }
37
38 /* C´alculo del producto */
39 if (columnas_a == filas_b) {
40 filas_p = filas_a;
41 columnas_p = columnas_b;
42 for (i=0; i<filas_p; i++)
43 for (j=0; j<columnas_p; j++) {
44 p[i][j] = 0.0;
45 for (k=0; k<columnas_a; k++)
46 p[i][j] += a[i][k] * b[k][j];
47 }
48 }
49
50 /* Impresi´on del resultado de la suma */
51 if (filas_a == filas_b && columnas_a == columnas_b) {
52 printf ("Suman");
53 for (i=0; i<filas_s; i++) {
54 for (j=0; j<columnas_s; j++)
55 printf ("%8.3f", s[i][j]);
56 printf ("n");
57 }
58 }
59 else
60 printf ("Matrices no compatibles para la suma.n");
61
62 /* Impresi´on del resultado del producto */
63 if (columnas_a == filas_b) {
64 printf ("Producton");
Introducci´on a la Programaci´on con C 115
2.3 Vectores multidimensionales
65 for (i=0; i<filas_p; i++) {
66 for (j=0; j<columnas_p; j++)
67 printf ("%8.3f", p[i][j]);
68 printf ("n");
69 }
70 }
71 else
72 printf ("Matrices no compatibles para el producto.n");
73
74 return 0;
75 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 129 Extiende el programa de calculadora matricial para efectuar las siguientes operaciones:
Producto de una matriz por un escalar. (La matriz resultante tiene la misma dimensi´on que
la original y cada elemento se obtiene multiplicando el escalar por la celda correspondiente
de la matriz original.)
Transpuesta de una matriz. (La transpuesta de una matriz de n × m es una matriz de
m × n en la que el elemento de la fila i y columna j tiene el mismo valor que el que ocupa
la celda de la fila j y columna i en la matriz original.)
· 130 Una matriz tiene un valle si el valor de una de sus celdas es menor que el de cualquiera
de sus 8 celdas vecinas. Dise˜na un programa que lea una matriz (el usuario te indicar´a de
cu´antas filas y columnas) y nos diga si la matriz tiene un valle o no. En caso afirmativo, nos
mostrar´a en pantalla las coordenadas de todos los valles, sus valores y el de sus celdas vecinas.
La matriz debe tener un n´umero de filas y columnas mayor o igual que 3 y menor o igual
que 10. Las casillas que no tienen 8 vecinos no se consideran candidatas a ser valle (pues no
tienen 8 vecinos).
Aqu´ı tienes un ejemplo de la salida esperada para esta matriz de 4 × 5:




1 2 9 5 5
3 2 9 4 5
6 1 8 7 6
6 3 8 0 9




Valle en fila 2 columna 4:
9 5 5
9 4 5
8 7 6
Valle en fila 3 columna 2:
3 2 9
6 1 8
6 3 8
(Observa que al usuario se le muestran filas y columnas numeradas desde 1, y no desde 0.)
· 131 Modifica el programa del ejercicio anterior para que considere candidata a valle a
cualquier celda de la matriz. Si una celda tiene menos de 8 vecinos, se considera que la celda
es valle si su valor es menor que el de todos ellos.
Para la misma matriz del ejemplo del ejercicio anterior se obtendr´ıa esta salida:
Valle en fila 1 columna 1:
x x x
x 1 2
x 3 2
Valle en fila 2 columna 4:
9 5 5
9 4 5
8 7 6
Valle en fila 3 columna 2:
3 2 9
6 1 8
116 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
6 3 8
Valle en fila 4 columna 4:
8 7 6
8 0 9
x x x
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3.3. Vectores de cadenas, matrices de caracteres
Por lo dicho hasta el momento, est´a claro que un vector de cadenas es una matriz de caracteres.
Este fragmento de programa, por ejemplo, declara un vector de 10 cadenas cuya longitud es
menor o igual que 80:
#define MAXLON 80
char v[10][MAXLON+1];
Cada fila de la matriz es una cadena y, como tal, debe terminar en un car´acter nulo.
Este fragmento declara e inicializa un vector de tres cadenas:
#define MAXLON 80
char v[3][MAXLON+1] = {"una",
"dos",
"tres" };
Puedes leer individualmente cada cadena por teclado:
matriz cadenas.c matriz cadenas.c
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char v[3][MAXLON+1];
8 int i;
9
10 for (i=0; i<3; i++) {
11 printf ("Introduzca cadena: ");
12 gets(v[i]);
13 printf ("Cadena le´ıda: %sn", v[i]);
14 }
15
16 return 0;
17 }
Vamos a desarrollar un programa ´util que hace uso de un vector de caracteres: un peque˜no
corrector ortogr´afico para ingl´es. El programa dispondr´a de una lista de palabras en ingl´es (que
encontrar´as en la p´agina web de la asignatura, en el fichero ingles.h), solicitar´a al usuario que
introduzca por teclado un texto en ingl´es y le informar´a de qu´e palabras considera err´oneas por
no estar inclu´ıdas en su diccionario. Aqu´ı tienes un ejemplo de uso del programa:
Introduce una frase: does this sentence contiene only correct words, eh?
palabra no encontrada: contiene
palabra no encontrada: eh
El fichero ingles.h es una cabecera de la que te mostramos ahora las primeras y ´ultimas
l´ıneas:
ingles.h ingles.h
1 #define DICCPALS 45378
2 #define MAXLONPAL 28
3 char diccionario[DICCPALS][MAXLONPAL+1] = {
Introducci´on a la Programaci´on con C 117
2.3 Vectores multidimensionales
4 "aarhus",
5 "aaron",
6 "ababa",
7 "aback",
8 "abaft",
9 "abandon",
10 "abandoned",
11 "abandoning",
12 "abandonment",
.
.
.
45376 "zorn",
45377 "zoroaster",
45378 "zoroastrian",
45379 "zulu",
45380 "zulus",
45381 "zurich"
45382 };
La variable diccionario es un vector de cadenas (o una matriz de caracteres, seg´un lo veas)
donde cada elemento es una palabra inglesa en min´usculas. La constante DICCPALS nos indica
el n´umero de palabras que contiene el diccionario y MAXLONPAL es la longitud de la palabra m´as
larga (28 bytes), por lo que reservamos espacio para MAXLONPAL+1 caracteres (29 bytes: 28 m´as
el correspondiente al terminador nulo).
Las primeras l´ıneas de nuestro programa son ´estas:
corrector.c
1 #include <stdio.h>
2 #include "ingles.h"
F´ıjate en que inclu´ımos el fichero ingles.h encerrando su nombre entre comillas dobles, y no
entre < y >. Hemos de hacerlo as´ı porque ingles.h es una cabecera nuestra y no reside en los
directorios est´andar del sistema (m´as sobre esto en el siguiente cap´ıtulo).
El programa empieza solicitando una cadena con gets. A continuaci´on, la dividir´a en un
nuevo vector de palabras. Supondremos que una frase no contiene m´as de 100 palabras y que
una palabra es una secuencia cualquiera de letras. Si el usuario introduce m´as de 100 palabras,
le advertiremos de que el programa s´olo corrige las 100 primeras. Una vez formada la lista
de palabras de la frase, el programa buscar´a cada una de ellas en el diccionario. Las que no
est´en, se mostrar´an en pantalla precedidas del mensaje: palabra no encontrada. Vamos all´a:
empezaremos por la lectura de la frase y su descomposici´on en una lista de palabras.
corrector 1.c corrector.c
1 #include <stdio.h>
2 #include "ingles.h"
3 #include <string.h>
4 #include <ctype.h>
5
6 #define MAXLONFRASE 1000
7 #define MAXPALSFRASE 100
8 #define MAXLONPALFRASE 100
9
10 int main(void)
11 {
12 char frase[MAXLONFRASE+1];
13 char palabra[MAXPALSFRASE][MAXLONPALFRASE+1];
14 int palabras; // N´umero de palabras en la frase
15 int lonfrase, i, j;
16
17 /* Lectura de la frase */
18 printf ("Introduce una frase: ");
19 gets(frase);
20
21 lonfrase = strlen(frase);
22
118 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
23 /* Descomposici´on en un vector de palabras */
24 i = 0;
25 while (i<lonfrase && !isalpha(frase[i])) i++; // Saltarse las no-letras iniciales.
26
27 palabras = 0;
28 while (i<lonfrase) { // Recorrer todos los caracteres
29
30 // Avanzar mientras vemos caracteres e ir formando la palabra palabra[palabras].
31 j = 0;
32 while (i<lonfrase && isalpha(frase[i])) palabra[palabras][j++] = frase[i++];
33 palabra[palabras][j] = ’0’; // El terminador es responsabilidad nuestra.
34
35 // Incrementar el contador de palabras.
36 palabras++;
37 if (palabras == MAXPALSFRASE) // Y finalizar si ya no caben m´as palabras
38 break;
39
40 // Saltarse las no-letras que separan esta palabra de la siguiente (si las hay).
41 while (i<lonfrase && !isalpha(frase[i])) i++;
42 }
43
44 /* Comprobaci´on de posibles errores */
45 for (i=0; i<palabras; i++)
46 printf ("%sn", palabra[i]);
47
48 return 0;
49 }
¡Buf! Complicado, ¿no? ¡Ya estamos echando en falta el m´etodo split de Python! No nos viene
mal probar si nuestro c´odigo funciona mostrando las palabras que ha encontrado en la frase.
Por eso hemos a˜nadido las l´ıneas 44–46. Una vez hayas ejecutado el programa y comprobado
que funciona correctamente hasta este punto, comenta el bucle que muestra las palabras:
44 /* Comprobaci´on de posibles errores */
45 // for (i=0; i<palabras; i++)
46 // printf ("%sn", palabra[i]);
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 132 Un programador, al copiar el programa, ha sustituido la l´ınea que reza as´ı:
while (i<lonfrase && !isalpha(frase[i])) i++; // Saltarse las no-letras iniciales.
por esta otra:
while (frase[i] != ’0’ && !isalpha(frase[i])) i++; // Saltarse las no-letras iniciales.
¿Es correcto el programa resultante? ¿Por qu´e?
· 133 Un programador, al copiar el programa, ha sustituido la l´ınea que reza as´ı:
while (i<lonfrase && !isalpha(frase[i])) i++; // Saltarse las no-letras iniciales.
por esta otra:
while (frase[i] && !isalpha(frase[i])) i++; // Saltarse las no-letras iniciales.
¿Es correcto el programa resultante? ¿Por qu´e?
· 134 Un programador, al copiar el programa, ha sustituido la l´ınea que reza as´ı:
while (i<lonfrase && isalpha(frase[i])) palabra[palabras][j++] = frase[i++];
por esta otra:
while (isalpha(frase[i])) palabra[palabras][j++] = frase[i++];
¿Es correcto el programa resultante? ¿Por qu´e?
Introducci´on a la Programaci´on con C 119
2.3 Vectores multidimensionales
· 135 Un programador, al copiar el programa, ha sustituido la l´ınea que reza as´ı:
while (i<lonfrase && !isalpha(frase[i])) i++; // Saltarse las no-letras iniciales.
por esta otra:
while (!isalpha(frase[i])) palabra[palabras][j++] = frase[i++];
¿Es correcto el programa resultante? ¿Por qu´e?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Sigamos. Nos queda la b´usqueda de cada palabra en el diccionario. Una primera idea consiste
en buscar cada palabra de la frase recorriendo el diccionario desde la primera hasta la ´ultima
entrada:
corrector 2.c corrector.c
.
.
.
48
49 /*
?
Est´an todas las palabras en el diccionario? */
50 for (i=0; i<palabras; i++) {
51 encontrada = 0;
52 for (j=0; j<DICCPALS; j++)
53 if (strcmp(palabra[i],diccionario[j]) == 0) { //
?
Es palabra[i] igual que diccionario[j]?
54 encontrada = 1;
55 break;
56 }
57 if (!encontrada)
58 printf ("palabra no encontrada: %sn", palabra[i]);
59 }
60 return 0;
61 }
Ten en cuenta lo que hace strcmp: recorre las dos cadenas hasta encontrar alguna diferencia
entre ellas o concluir que son id´enticas. Es, por tanto, una operaci´on bastante costosa en tiempo.
¿Podemos reducir el n´umero de comparaciones? ¡Claro! Como el diccionario est´a ordenado al-
fab´eticamente, podemos abortar el recorrido cuando llegamos a una voz del diccionario posterior
(seg´un el orden alfab´etico) a la que buscamos:
corrector 3.c corrector.c
.
.
.
48
49 /*
?
Est´an todas las palabras en el diccionario? */
50 for (i=0; i<palabras; i++) {
51 encontrada = 0;
52 for (j=0; j<DICCPALS; j++)
53 if (strcmp(palabra[i],diccionario[j]) == 0) { //
?
Es palabra[i] igual que diccionario[j]?
54 encontrada = 1;
55 break;
56 }
57 else if (strcmp(palabra[i], diccionario[j]) < 0) //
?
palabra[i] < diccionario[j]?
58 break;
59 if (!encontrada)
60 printf ("palabra no encontrada: %sn", palabra[i]);
61 }
62 return 0;
63 }
Con esta mejora hemos intentado reducir a la mitad el n´umero de comparaciones con cadenas
del diccionario, pero no hemos logrado nuestro objetivo: ¡aunque, en promedio, efectuamos
comparaciones con la mitad de las palabras del diccionario, estamos llamando dos veces a
strcmp! Es mejor almacenar el resultado de una sola llamada a strcmp en una variable:
corrector 4.c corrector.c
.
.
.
48
120 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
49 /*
?
Est´an todas las palabras en el diccionario? */
50 for (i=0; i<palabras; i++) {
51 encontrada = 0;
52 for (j=0; j<DICCPALS; j++) {
53 comparacion = strcmp(palabra[i], diccionario[j]);
54 if (comparacion == 0) { //
?
Es palabra[i] igual que diccionario[j]?
55 encontrada = 1; break;
56 }
57 else if (comparacion < 0) //
?
Es palabra[i] menor que diccionario[j]?
58 break;
59 }
60 if (!encontrada)
61 printf ("palabra no encontrada: %sn", palabra[i]);
62 }
63 return 0;
64 }
(Recuerda declarar comparacion como variable de tipo entero.)
El diccionario tiene 45378 palabras. En promedio efectuamos, pues, 22689 comparaciones
por cada palabra de la frase. Mmmm. A´un podemos hacerlo mejor. Si la lista est´a ordenada,
podemos efectuar una b´usqueda dicot´omica. La b´usqueda dicot´omica efect´ua un n´umero de
comparaciones reducid´ısimo: ¡bastan 16 comparaciones para decidir si una palabra cualquiera
est´a o no en el diccionario!
corrector.c
.
.
.
97
98 /*
?
Est´an todas las palabras en el diccionario? */
99 for (i=0; i<palabras; i++) {
100 encontrada = 0;
101 izquierda = 0;
102 derecha = DICCPALS;
103
104 while (izquierda < derecha) {
105 j = (izquierda + derecha) / 2;
106 comparacion = strcmp(palabra[i], diccionario[j]);
107 if (comparacion < 0)
108 derecha = j;
109 else if (comparacion > 0)
110 izquierda = j+1;
111 else {
112 encontrada = 1;
113 break;
114 }
115 }
116
117 if (!encontrada)
118 printf ("palabra no encontrada: %sn", palabra[i]);
119 }
120
121 return 0;
122 }
(Debes declarar derecha e izquierda como enteros.)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 136 Escribe un programa C que lea un texto (de longitud menor que 1000) y obtenga un
vector de cadenas en el que cada elemento es una palabra distinta del texto (con un m´aximo
de 500 palabras). Muestra el contenido del vector por pantalla.
· 137 Modifica el programa del ejercicio anterior para que el vector de palabras se muestre
en pantalla ordenado alfab´eticamente. Deber´as utilizar el m´etodo de la burbuja para ordenar
el vector.
Introducci´on a la Programaci´on con C 121
2.3 Vectores multidimensionales
· 138 Representamos la baraja de cartas con un vector de cadenas. Los palos son "oros ",
"copas", "espadas" y "bastos". Las cartas con n´umeros entre 2 y 9 se describen con el texto
"n´umero de palo" (ejemplo: "2 de oros", "6 de copas"). Los ases se describen con la cadena
"as de palo", las sotas con "sota de palo", los caballos con "caballo de palo" y los reyes
con "rey de palo".
Escribe un programa que genere la descripci´on de las 40 cartas de la baraja. Usa bucles
siempre que puedas y comp´on las diferentes partes de cada descripci´on con strcat o sprintf . A
continuaci´on, baraja las cartas utilizando para ello el generador de n´umeros aleatorios y muestra
el resultado por pantalla.
· 139 Dise˜na un programa de ayuda al diagn´ostico de enfermedades. En nuestra base de
datos hemos registrado 10 enfermedades y 10 s´ıntomas:
1 char enfermedades[10][20] = { "gripe", "indigesti´on", "catarro", ... };
2 char sintomas[10][20] = { "fiebre", "tos", "dolor de cabeza", ... };
Almacenamos en una matriz de 10 × 10 valores booleanos (1 o 0) los s´ıntomas que presenta
cada enfermedad:
1 char sintomatologia[10][10] = {{ 1, 0, 1, ... },
2 { 0, 0, 0, ... },
3 ...
4 };
La celda sintomatologia[i][j] vale 1 si la enfermedad i presenta el s´ıntoma j, y 0 en caso
contrario.
Dise˜na un programa que pregunte al paciente si sufre cada uno de los 10 s´ıntomas y, en
funci´on de las respuestas dadas, determine la enfermedad que padece. Si la descripci´on de sus
s´ıntomas no coincide exactamente con la de alguna de las enfermedades, el sistema indicar´a que
no se puede emitir un diagn´ostico fiable.
· 140 Modifica el programa anterior para que, cuando no hay coincidencia absoluta de
s´ıntomas, muestre las tres enfermedades con sintomatolog´ıa m´as parecida. Si, por ejemplo, una
enfermedad presenta 9 coincidencias con la sintomatolog´ıa del paciente, el sistema mostrar´a el
nombre de la enfermedad y el porcentaje de confianza del diagn´ostico (90%).
· 141 Vamos a implementar un programa que nos ayude a traducir texto a c´odigo Morse.
Aqu´ı tienes una tabla con el c´odigo Morse:
A B C D E F G H I J K L
.- -... -.-. -.. . ..-. --. .... .. .--- -.- .-..
M N O P Q R S T U V W X
-- -. --- .--. --.- .-. ... - ..- ...- .-- -..-
Y Z 0 1 2 3 4 5 6 7 8 9
-.-- --.. ----- .---- ..--- ...-- ....- ..... -.... --... ---.. ----.
El programa leer´a una l´ınea y mostrar´a por pantalla su traducci´on a c´odigo Morse. Ten en
cuenta que las letras se deben separar por pausas (un espacio blanco) y las palabras por pausas
largas (tres espacios blancos). Los acentos no se tendr´an en cuenta al efectuar la traducci´on (la
letra ´A, por ejemplo, se representar´a con .-) y la letra ’~N’ se mostrar´a como una ’N’. Los signos
que no aparecen en la tabla (comas, admiraciones, etc.) no se traducir´an, excepci´on hecha del
punto, que se traduce por la palabra STOP. Te conviene pasar la cadena a may´usculas (o efectuar
esta transformaci´on sobre la marcha), pues la tabla Morse s´olo recoge las letras may´usculas y
los d´ıgitos.
Por ejemplo, la cadena "Hola, mundo." se traducir´a por
.... --- .-.. .- -- ..- -. -.. --- ... - --- .--.
Debes usar un vector de cadenas para representar la tabla de traducci´on a Morse. El c´odigo
Morse de la letra ’A’, por ejemplo, estar´a accesible como una cadena en morse[’A’].
(Tal vez te sorprenda la notaci´on morse[’A’]. Recuerda que ’A’ es el n´umero 65, pues
el car´acter ’A’ tiene ese valor ASCII. As´ı pues, morse[’A’] y morse[65] son lo mismo. Por
cierto: el vector de cadenas morse s´olo tendr´a c´odigos para las letras may´usculas y los d´ıgitos;
recuerda inicializar el resto de componentes con la cadena vac´ıa.)
122 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
· 142 Escribe un programa que lea un texto escrito en c´odigo Morse y lo traduzca al c´odigo
alfab´etico.
Si, por ejemplo, el programa lee por teclado esta cadena:
".... --- .-.. .- -- ..- -. -.. --- ... - --- .--."
mostrar´a en pantalla el texto HOLAMUNDOSTOP.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.4. Registros
Los vectores permiten agrupar varios elementos de un mismo tipo. Cada elemento de un vector
es accesible a trav´es de un ´ındice.
En ocasiones necesitar´as agrupar datos de diferentes tipos y/o preferir´as acceder a diferentes
elementos de un grupo de datos a trav´es de un identificador, no de un ´ındice. Los registros
son agrupaciones heterog´eneas de datos cuyos elementos (denominados campos) son accesibles
mediante identificadores. Ya hemos estudiado registros en Python, as´ı que el concepto y su
utilidad han de resultarte familiares.
Veamos ahora un dise˜no t´ıpico de registro. Supongamos que deseamos mantener los siguien-
tes datos de una persona:
su nombre (con un m´aximo de 40 caracteres),
su edad (un entero),
su DNI (una cadena de 9 caracteres).
Podemos definir un registro ((persona)) antes de la aparici´on de main:
#define MAXNOM 40
#define LONDNI 9
struct Persona {
char nombre[MAXNOM+1];
int edad;
char dni[LONDNI+1];
}; // <- F´ıjate en el punto y coma: es f´acil olvidarse de ponerlo.
La definici´on de un registro introduce un nuevo tipo de datos en nuestro programa. En el
ejemplo hemos definido el tipo struct Persona (la palabra struct forma parte del nombre del
tipo). Ahora puedes declarar variables de tipo struct Persona as´ı:
struct Persona pepe, juan, ana;
En tu programa puedes acceder a cada uno de los campos de una variable de tipo struct sepa-
rando con un punto el identificador de la variable del correspondiente identificador del campo.
Por ejemplo, pepe.edad es la edad de Pepe (un entero que ocupa cuatro bytes), juan.nombre es
el nombre de Juan (una cadena), y ana.dni[8] es la letra del DNI de Ana (un car´acter).
Cada variable de tipo struct Persona ocupa, en principio, 55 bytes: 41 por el nombre, 4
por la edad y 10 por el DNI. (Si quieres saber por qu´e hemos resaltado lo de ((en principio)), lee
el cuadro ((Alineamientos)).)
Este programa ilustra c´omo acceder a los campos de un registro leyendo por teclado sus
valores y mostrando por pantalla diferentes informaciones almacenadas en ´el:
registro.c
1 #include <stdio.h>
2 #include <string.h>
3
4 #define MAXNOM 40
5 #define LONDNI 9
6
7 struct Persona {
8 char nombre[MAXNOM+1];
9 int edad;
Introducci´on a la Programaci´on con C 123
2.4 Registros
Alineamientos
El operador sizeof devuelve el tama˜no en bytes de un tipo o variable. Analiza este programa:
alineamiento.c alineamiento.c
1 #include <stdio.h>
2
3 struct Registro {
4 char a;
5 int b;
6 };
7
8 int main(void)
9 {
10 printf ("Ocupaci´on: %d bytesn", sizeof(struct Registro));
11 return 0;
12 }
Parece que vaya a mostrar en pantalla el mensaje ((Ocupaci´on: 5 bytes)), pues un char
ocupa 1 byte y un int ocupa 4. Pero no es as´ı:
Ocupaci´on: 8 bytes
La raz´on de que ocupe m´as de lo previsto es la eficiencia. Los ordenadores con arqui-
tectura de 32 bits agrupan la informaci´on en bloques de 4 bytes. Cada uno de esos bloques
se denomina ((palabra)). Cada acceso a memoria permite traer al procesador los 4 bytes de
una palabra. Si un dato est´a a caballo entre dos palabras, requiere dos accesos a memoria,
afectando seriamente a la eficiencia del programa. El compilador trata de generar un pro-
grama eficiente y da prioridad a la velocidad de ejecuci´on frente al consumo de memoria. En
nuestro caso, esta prioridad se ha traducido en que el segundo campo se almacene en una
palabra completa, aunque ello suponga desperdiciar 3 bytes en el primero de los campos.
10 char dni[LONDNI+1];
11 };
12
13 int main(void)
14 {
15 struct Persona ejemplo;
16 char linea[81];
17 int i, longitud;
18
19 printf ("Nombre: "); gets(ejemplo.nombre);
20 printf ("Edad : "); gets(linea); sscanf (linea, "%d", &ejemplo.edad);
21 printf ("DNI : "); gets(ejemplo.dni);
22
23 printf ("Nombre le´ıdo: %sn", ejemplo.nombre);
24 printf ("Edad le´ıda : %dn", ejemplo.edad);
25 printf ("DNI le´ıdo : %sn", ejemplo.dni);
26
27 printf ("Iniciales del nombre: ");
28 longitud = strlen(ejemplo.nombre);
29 for (i=0; i<longitud; i++)
30 if (ejemplo.nombre[i] >= ’A’ && ejemplo.nombre[i] <= ’Z’)
31 printf ("%c", ejemplo.nombre[i]);
32 printf ("n");
33
34 printf ("Letra del DNI: ");
35 longitud = strlen(ejemplo.dni);
36 if (ejemplo.dni[longitud-1] < ’A’ || ejemplo.dni[longitud-1] > ’Z’)
37 printf ("No tiene letra.n");
38 else
39 printf ("%cn", ejemplo.dni[longitud-1]);
124 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
40
41 return 0;
42 }
Los registros pueden copiarse´ıntegramente sin mayor problema. Este programa, por ejemplo,
copia el contenido de un registro en otro y pasa a min´usculas el nombre de la copia:
copia registro.c copia registro.c
1 #include <stdio.h>
2 #include <string.h>
3 #include <ctype.h>
4
5 #define MAXNOM 40
6 #define LONDNI 9
7
8 struct Persona {
9 char nombre[MAXNOM+1];
10 int edad;
11 char dni[LONDNI+1];
12 };
13
14 int main(void)
15 {
16 struct Persona una, copia;
17 char linea[81];
18 int i, longitud;
19
20 printf ("Nombre: "); gets(una.nombre);
21 printf ("Edad : "); gets(linea); sscanf (linea, "%d", &una.edad);
22 printf ("DNI : "); gets(una.dni);
23
24 copia = una; // Copia
25
26 longitud = strlen(copia.nombre);
27 for (i=0; i<longitud; i++)
28 copia.nombre[i] = tolower(copia.nombre[i]);
29
30 printf ("Nombre le´ıdo: %sn", una.nombre);
31 printf ("Edad le´ıda : %dn", una.edad);
32 printf ("DNI le´ıdo : %sn", una.dni);
33
34 printf ("Nombre copia: %sn", copia.nombre);
35 printf ("Edad copia : %dn", copia.edad);
36 printf ("DNI copia : %sn", copia.dni);
37
38 return 0;
39 }
Observa que la copia se efect´ua incluso cuando los elementos del registro son vectores. O
sea, copiar vectores con una mera asignaci´on est´a prohibido, pero copiar registros es posible.
Un poco incoherente, ¿no?
Por otra parte, no puedes comparar registros. Este programa, por ejemplo, efect´ua una copia
de un registro en otro para, a continuaci´on, intentar decirnos si ambos son iguales o no:
E compara registros mal.c E
1 #include <stdio.h>
2
3 #define MAXNOM 40
4 #define LONDNI 9
5
6 struct Persona {
7 char nombre[MAXNOM+1];
8 int edad;
9 char dni[LONDNI+1];
Introducci´on a la Programaci´on con C 125
2.4 Registros
10 };
11
12 int main(void)
13 {
14 struct Persona una, copia;
15 char linea[81];
16 int i, longitud;
17
18 printf ("Nombre: "); gets(una.nombre);
19 printf ("Edad : "); gets(linea); sscanf (linea, "%d", &una.edad);
20 printf ("DNI : "); gets(una.dni);
21
22 copia = una; // Copia
23
24 if (copia == una ) // Comparaci´on ilegal.
25 printf ("Son igualesn");
26 else
27 printf ("No son igualesn");
28
29 return 0;
30 }
Pero ni siquiera es posible compilarlo. La l´ınea 24 contiene un error que el compilador se˜nala co-
mo ((invalid operands to binary ==)), o sea, ((operandos inv´alidos para la operaci´on binaria
==)). Entonces, ¿c´omo podemos decidir si dos registros son iguales? Comprobando la igualdad
de cada uno de los campos de un registro con el correspondiente campo del otro:
compara registros.c compara registros.c
1 #include <stdio.h>
2
3 #define MAXNOM 40
4 #define LONDNI 9
5
6 struct Persona {
7 char nombre[MAXNOM+1];
8 int edad;
9 char dni[LONDNI+1];
10 };
11
12 int main(void)
13 {
14 struct Persona una, copia;
15 char linea[81];
16 int i, longitud;
17
18 printf ("Nombre: "); gets(una.nombre);
19 printf ("Edad : "); gets(linea); scanf (linea, "%d", &una.edad);
20 printf ("DNI : "); gets(una.dni);
21
22 copia = una; // Copia
23
24 if (strcmp(copia.nombre, una.nombre)==0 && copia.edad==una.edad
25 && strcmp(copia.dni, una.dni)==0)
26 printf ("Son igualesn");
27 else
28 printf ("No son igualesn");
29
30 return 0;
31 }
126 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
Una raz´on para no comparar
Si C sabe copiar una estructura ((bit a bit)), ¿por qu´e no sabe compararlas ((bit a bit))?
El problema estriba en construcciones como las cadenas que son campos de un registro.
Considera esta definici´on:
struct Persona {
char nombre[10];
char apellido[10];
};
Cada dato de tipo struct Persona ocupa 20 bytes. Si una persona a tiene su campo
a.nombre con valor "Pepe", s´olo los cinco primeros bytes de su nombre tienen un valor
bien definido. Los cinco siguientes pueden tener cualquier valor aleatorio. Otro registro b
cuyo campo b.nombre tambi´en valga "Pepe" (y tenga id´entico apellido) puede tener valores
diferentes en su segundo grupo de cinco bytes. Una comparaci´on ((bit a bit)) nos dir´ıa que
los registros son diferentes.
La asignaci´on no entra˜na este tipo de problema, pues la copia es ((bit a bit)). Como
mucho, resulta algo ineficiente, pues copiar´a hasta los bytes de valor indefinido.
Una forma de inicializaci´on
C permite inicializar registros de diferentes modos, algunos bastante interesantes desde el
punto de vista de la legibilidad. Este programa, por ejemplo, define un struct y crea e
inicializa de diferentes formas, pero con el mismo valor, varias variables de este tipo:
struct Algo {
int x;
char nombre[10];
float y;
};
...
struct Algo a = { 1, "Pepe", 2.0 };
struct Algo b = { .x = 1, .nombre = "Pepe", .y = 2.0 };
struct Algo c = { .nombre = "Pepe", .y = 2.0, .x = 1};
struct Algo d;
...
d.x = 1;
strcpy(d.nombre, "Pepe");
d.y = 2.0;
2.4.1. Un ejemplo: registros para almacenar vectores de talla variable (pero
acotada)
Los vectores est´aticos tienen una talla fija. Cuando necesitamos un vector cuya talla var´ıa o no
se conoce hasta iniciada la ejecuci´on del programa usamos un truco: definimos un vector cuya
talla sea suficientemente grande para la tarea que vamos a abordar y mantenemos la ((talla real))
en una variable. Lo hemos hecho con el programa que calcula algunas estad´ısticas con una serie
de edades: defin´ıamos un vector edad con capacidad para almacenar la edad de MAX_PERSONAS
y una variable personas, cuyo valor siempre era menor o igual que MAX_PERSONAS, nos indicaba
cu´antos elementos del vector conten´ıan realmente datos. Hay algo poco elegante en esa soluci´on:
las variables edad y personas son variables independientes, que no est´an relacionadas entre s´ı
en el programa (salvo por el hecho de que nosotros sabemos que s´ı lo est´an). Una soluci´on m´as
elegante pasa por crear un registro que contenga el n´umero de personas y, en un vector, las
edades. He aqu´ı el programa que ya te presentamos en su momento convenientemente modificado
seg´un este nuevo principio de dise˜no:
Introducci´on a la Programaci´on con C 127
2.4 Registros
edades 8.c edades.c
1 #include <stdio.h>
2 #include <math.h>
3
4 #define MAX_PERSONAS 20
5
6 struct ListaEdades {
7 int edad[MAX_PERSONAS]; // Vector con capacidad para MAX PERSONAS edades.
8 int talla; // N´umero de edades realmente almacenadas.
9 };
10
11 int main(void)
12 {
13 struct ListaEdades personas;
14 int i, j, aux, suma_edad;
15 float suma_desviacion, media, desviacion;
16 int moda, frecuencia, frecuencia_moda, mediana;
17
18 /* Lectura de edades */
19 personas.talla = 0;
20 do {
21 printf ("Introduce edad de la persona %d (si es negativa, acabar): ",
22 personas.talla +1);
23 scanf ("%d", &personas.edad[personas.talla ]);
24 personas.talla ++;
25 } while (personas.talla < MAX_PERSONAS && personas.edad [personas.talla -1] >= 0);
26 personas.talla --;
27
28 if (personas.talla > 0) {
29 /* C´alculo de la media */
30 suma_edad = 0;
31 for (i=0; i<personas.talla ; i++)
32 suma_edad += personas.edad[i];
33 media = suma_edad / (float)personas.talla ;
34
35 /* C´alculo de la desviacion t´ıpica */
36 suma_desviacion = 0.0;
37 for (i=0; i<personas.talla ; i++)
38 suma_desviacion += (personas.edad[i] - media) * (personas.edad[i] - media);
39 desviacion = sqrt( suma_desviacion / personas.talla );
40
41 /* C´alculo de la moda */
42 for (i=0; i<personas.talla -1; i++) // Ordenaci´on mediante burbuja.
43 for (j=0; j<personas.talla -i; j++)
44 if (personas.edad[j] > personas.edad[j+1]) {
45 aux = personas.edad[j];
46 personas.edad[j] = personas.edad[j+1];
47 personas.edad[j+1] = aux;
48 }
49
50 frecuencia = 0;
51 frecuencia_moda = 0;
52 moda = -1;
53 for (i=0; i<personas.talla -1; i++)
54 if (personas.edad[i] == personas.edad[i+1])
55 if (++frecuencia > frecuencia_moda) {
56 frecuencia_moda = frecuencia;
57 moda = personas.edad[i];
58 }
59 else
60 frecuencia = 0;
61
62 /* C´alculo de la mediana */
128 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
63 mediana = personas.edad[personas.talla/2];
64
65 /* Impresi´on de resultados */
66 printf ("Edad media : %fn", media);
67 printf ("Desv. t´ıpica: %fn", desviacion);
68 printf ("Moda : %dn", moda);
69 printf ("Mediana : %dn", mediana);
70 }
71 else
72 printf ("No se introdujo dato alguno.n");
73
74 return 0;
75 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 143 Modifica el programa de c´alculo con polinomios que sirvi´o de ejemplo en el apar-
tado 2.1.5 para representar los polinomios mediante registros. Cada registro contendr´a dos
campos: el grado del polinomio y el vector con los coeficientes.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.4.2. Un ejemplo: rectas de regresi´on para una serie de puntos en el plano
Hay m´etodos estad´ısticos que permiten obtener una recta que se ajusta de forma ´optima a una
serie de puntos en el plano.
y = mx + b
Si disponemos de una serie de n puntos (x1, y1), (x2, y2), . . . , (xn, yn), la recta de ajuste y =
mx+b que minimiza el cuadrado de la distancia vertical de todos los puntos a la recta se puede
obtener efectuando los siguientes c´alculos:
m =
(
n
i=1 xi) · (
n
i=1 yi) − n ·
n
i=1 xiyi
(
n
i=1 xi)
2
− n ·
n
i=1 x2
i
,
b =
(
n
i=1 yi) ·
n
i=1 x2
i − (
n
i=1 xi) · (
n
i=1 xiyi)
n
n
i=1 x2
i − (
n
i=1 xi)
2 .
Las f´ormulas asustan un poco, pero no contienen m´as que sumatorios. El programa que vamos
a escribir lee una serie de puntos (con un n´umero m´aximo de, pongamos, 1000), y muestra los
valores de m y b.
Modelaremos los puntos con un registro:
struct Punto {
float x, y;
};
El vector de puntos, al que en principio denominaremos p, tendr´a talla 1000:
#define TALLAMAX 1000
struct Punto p[TALLAMAX];
Pero 1000 es el n´umero m´aximo de puntos. El n´umero de puntos disponibles efectivamente ser´a
menor o igual y su valor deber´a estar accesible en alguna variable. Olvid´emonos del vector p:
nos conviene definir un registro en el que se almacenen vector y talla real del vector.
Introducci´on a la Programaci´on con C 129
2.4 Registros
struct ListaPuntos {
struct Punto punto[TALLAMAX];
int talla;
};
Observa que estamos anidando structs.
Necesitamos ahora una variable del tipo que hemos definido:
1 #include <stdio.h>
2
3 #define TALLAMAX 1000
4
5 struct Punto {
6 float x, y;
7 };
8
9 struct ListaPuntos {
10 struct Punto punto[TALLAMAX];
11 int talla;
12 };
13
14 int main(void)
15 {
16 struct ListaPuntos lista ;
17 ...
Reflexionemos brevemente sobre c´omo podemos acceder a la informaci´on de la variable lista:
Expresi´on Tipo y significado
lista Es un valor de tipo struct ListaPuntos. Contiene un vector
de 1000 puntos y un entero.
lista.talla Es un entero. Indica cu´antos elementos del vector contienen
informaci´on.
lista.punto Es un vector de 1000 valores de tipo struct Punto.
lista.punto[0] Es el primer elemento del vector y es de tipo struct Punto,
as´ı que est´a compuesto por dos flotantes.
lista.punto[0].x Es el campo x del primer elemento del vector. Su tipo es
float.
lista.punto[lista.talla-1].y Es el campo y del ´ultimo elemento con informaci´on del vec-
tor. Su tipo es float.
lista.punto.x ¡Error! Si lista.punto es un vector, no podemos acceder al
campo x.
lista.punto.x[0] ¡Error! Si lo anterior era incorrecto, ´esto lo es a´un m´as.
lista.punto.[0].x ¡Error! ¿Qu´e hace un punto antes del operador de indexa-
ci´on?
lista[0].punto ¡Error! La variable lista no es un vector, as´ı que no puedes
aplicar el operador de indexaci´on sobre ella.
Ahora que tenemos m´as claro c´omo hemos modelado la informaci´on, vamos a resolver el
problema propuesto. Cada uno de los sumatorios se precalcular´a cuando se hayan le´ıdo los
puntos. De ese modo, simplificaremos significativamente las expresiones de c´alculo de m y b.
Debes tener en cuenta que, aunque en las f´ormulas se numeran los puntos empezando en 1, en
C se empieza en 0.
Veamos el programa completo:
ajuste.c ajuste.c
1 #include <stdio.h>
2
3 #define TALLAMAX 1000
4
5 struct Punto {
6 float x, y;
130 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
7 };
8
9 struct ListaPuntos {
10 struct Punto punto[TALLAMAX];
11 int talla;
12 };
13
14 int main(void)
15 {
16 struct ListaPuntos lista;
17
18 float sx, sy, sxy, sxx;
19 float m, b;
20 int i;
21
22 /* Lectura de puntos */
23 printf ("Puntos a leer: "); scanf ("%d", &lista.talla);
24 for (i=0; i<lista.talla; i++) {
25 printf ("Coordenada x del punto %d: ", i); scanf ("%f", &lista.punto[i].x);
26 printf ("Coordenada y del punto %d: ", i); scanf ("%f", &lista.punto[i].y);
27 }
28
29 /* C´alculo de los sumatorios */
30 sx = 0.0;
31 for (i=0; i<lista.talla; i++)
32 sx += lista.punto[i].x;
33
34 sy = 0.0;
35 for (i=0; i<lista.talla; i++)
36 sy += lista.punto[i].y;
37
38 sxy = 0.0;
39 for (i=0; i<lista.talla; i++)
40 sxy += lista.punto[i].x * lista.punto[i].y;
41
42 sxx = 0.0;
43 for (i=0; i<lista.talla; i++)
44 sxx += lista.punto[i].x * lista.punto[i].x;
45
46 /* C´alculo de m y b e impresi´on de resultados */
47 if (sx * sx - lista.talla * sxx == 0)
48 printf ("Indefinidan");
49 else {
50 m = (sx * sy - lista.talla * sxy) / (sx * sx - lista.talla * sxx);
51 printf ("m = %fn", m);
52 b = (sy * sxx - sx * sxy) / (lista.talla * sxx - sx * sx);
53 printf ("b = %fn", b);
54 }
55
56 return 0;
57 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 144 Dise˜na un programa que lea una lista de hasta 1000 puntos por teclado y los almacene
en una variable (del tipo que t´u mismo definas) llamada representantes. A continuaci´on, ir´a
leyendo nuevos puntos hasta que se introduzca el punto de coordenadas (0, 0). Para cada nuevo
punto, debes encontrar cu´al es el punto m´as pr´oximo de los almacenados en representantes.
Calcula la distancia entre dos puntos como la distancia eucl´ıdea.
· 145 Deseamos efectuar c´alculos con enteros positivos de hasta 1000 cifras, m´as de las que
puede almacenar un int (o incluso long long int). Define un registro que permita representar
n´umeros de hasta 1000 cifras con un vector en el que cada elemento es una cifra (representada
con un char). Representa el n´umero de cifras que tiene realmente el valor almacenado con un
Introducci´on a la Programaci´on con C 131
2.4 Registros
campo del registro. Escribe un programa que use dos variables del nuevo tipo para leer dos
n´umeros y que calcule el valor de la suma y la resta de estos (supondremos que la resta siempre
proporciona un entero positivo como resultado).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.4.3. Otro ejemplo: gesti´on de una coleci´on de CDs
Estamos en condiciones de abordar la implementaci´on de un programa moderadamente com-
plejo: la gesti´on de una colecci´on de CDs (aunque, todav´ıa, sin poder leer/escribir en fichero).
De cada CD almacenaremos los siguientes datos:
el t´ıtulo (una cadena con, a lo sumo, 80 caracteres),
el int´erprete (una cadena con, a lo sumo, 40 caracteres),
la duraci´on (en minutos y segundos),
el a˜no de publicaci´on.
Definiremos un registro para almacenar los datos de un CD y otro para representar la duraci´on,
ya que ´esta cuenta con dos valores (minutos y segundos):
#define LONTITULO 80
#define LONINTERPRETE 40
struct Tiempo {
int minutos;
int segundos;
};
struct CompactDisc {
char titulo[LONTITULO+1];
char interprete[LONINTERPRETE+1];
struct Tiempo duracion;
int anyo;
};
Vamos a usar un vector para almacenar la colecci´on, definiremos un m´aximo n´umero de CDs:
1000. Eso no significa que la colecci´on tenga 1000 discos, sino que puede tener a lo sumo 1000.
¿Y cu´antos tiene en cada instante? Utilizaremos una variable para mantener el n´umero de CDs
presente en la colecci´on. Mejor a´un: definiremos un nuevo tipo de registro que represente a la
colecci´on entera de CDs. El nuevo tipo contendr´a dos campos:
el vector de discos (con capacidad limitada a 1000 unidades),
y el n´umero de discos en el vector.
He aqu´ı la definici´on de la estructura y la declaraci´on de la colecci´on de CDs:
#define MAXDISCOS 1000
...
struct Coleccion {
struct CompactDisc cd[MAXDISCOS];
int cantidad;
};
struct Coleccion mis_cds;
Nuestro programa permitir´a efectuar las siguientes acciones:
A˜nadir un CD a la base de datos.
Listar toda la base de datos.
132 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
Listar los CDs de un int´erprete.
Suprimir un CD dado su t´ıtulo y su int´erprete.
(El programa no resultar´a muy ´util hasta que aprendamos a utilizar ficheros en C, pues al
finalizar cada ejecuci´on se pierde toda la informaci´on registrada.)
He aqu´ı el programa completo:
discoteca.c discoteca.c
1 #include <stdio.h>
2 #include <string.h>
3
4 #define MAXLINEA 80
5
6 #define MAXDISCOS 1000
7 #define LONTITULO 80
8 #define LONINTERPRETE 40
9
10 enum { Anyadir=1, ListadoCompleto, ListadoPorInterprete, Suprimir, Salir };
11
12 struct Tiempo {
13 int minutos;
14 int segundos;
15 };
16
17 struct CompactDisc {
18 char titulo[LONTITULO+1];
19 char interprete[LONINTERPRETE+1];
20 struct Tiempo duracion;
21 int anyo;
22 };
23
24 struct Coleccion {
25 struct CompactDisc cd[MAXDISCOS];
26 int cantidad;
27 };
28
29 int main(void)
30 {
31 struct Coleccion mis_cds;
32 int opcion, i, j;
33 char titulo[LONTITULO+1], interprete[LONINTERPRETE+1];
34 char linea[MAXLINEA]; // Para evitar los problemas de scanf.
35
36 /* Inicializaci´on de la colecci´on. */
37 mis_cds.cantidad = 0;
38
39 /* Bucle principal: men´u de opciones. */
40 do {
41 do {
42 printf ("Colecci´on de CDsn");
43 printf ("----------------n");
44 printf ("1) A~nadir CDn");
45 printf ("2) Listar todon");
46 printf ("3) Listar por int´erpreten");
47 printf ("4) Suprimir CDn");
48 printf ("5) Salirn");
49 printf ("Opci´on: ");
50 gets(linea); sscanf (linea, "%d", &opcion);
51 if (opcion <1 || opcion >5)
52 printf ("Opci´on inexistente. Debe estar entre 1 y 5n");
53 } while (opcion <1 || opcion >5);
54
55 switch(opcion) {
Introducci´on a la Programaci´on con C 133
2.4 Registros
56 case Anyadir: // A˜nadir un CD.
57 if (mis_cds.cantidad == MAXDISCOS)
58 printf ("La base de datos est´a llena. Lo siento.n");
59 else {
60 printf ("T´ıtulo: ");
61 gets(mis_cds.cd[mis_cds.cantidad].titulo);
62 printf ("Int´erprete: ");
63 gets(mis_cds.cd[mis_cds.cantidad].interprete);
64 printf ("Minutos: ");
65 gets(linea); sscanf (linea, "%d", &mis_cds.cd[mis_cds.cantidad].duracion.minutos);
66 printf ("Segundos: ");
67 gets(linea); sscanf (linea, "%d", &mis_cds.cd[mis_cds.cantidad].duracion.segundos);
68 printf ("A~no: ");
69 gets(linea); sscanf (linea, "%d", &mis_cds.cd[mis_cds.cantidad].anyo);
70 mis_cds.cantidad++;
71 }
72 break;
73
74 case ListadoCompleto: // Listar todo.
75 for (i=0; i<mis_cds.cantidad; i++)
76 printf ("%d %s de %s (%d:%d) %dn", i, mis_cds.cd[i].titulo,
77 mis_cds.cd[i].interprete,
78 mis_cds.cd[i].duracion.minutos,
79 mis_cds.cd[i].duracion.segundos,
80 mis_cds.cd[i].anyo);
81 break;
82
83 case ListadoPorInterprete: // Listar por int´erprete.
84 printf ("Int´erprete: "); gets(interprete);
85 for (i=0; i<mis_cds.cantidad; i++)
86 if (strcmp(interprete, mis_cds.cd[i].interprete) == 0)
87 printf ("%d %s de %s (%d:%d) %dn", i, mis_cds.cd[i].titulo,
88 mis_cds.cd[i].interprete,
89 mis_cds.cd[i].duracion.minutos,
90 mis_cds.cd[i].duracion.segundos,
91 mis_cds.cd[i].anyo);
92 break;
93
94 case Suprimir: // Suprimir CD.
95 printf ("T´ıtulo: "); gets(titulo);
96 printf ("Int´erprete: "); gets(interprete);
97 for (i=0; i<mis_cds.cantidad; i++)
98 if (strcmp(titulo, mis_cds.cd[i].titulo) == 0 &&
99 strcmp(interprete, mis_cds.cd[i].interprete) == 0)
100 break;
101 if (i < mis_cds.cantidad) {
102 for (j=i+1; j<mis_cds.cantidad; j++)
103 mis_cds.cd[j-1] = mis_cds.cd[j];
104 mis_cds.cantidad--;
105 }
106 break;
107 }
108
109 } while (opcion != Salir);
110 printf ("Gracias por usar nuestro programa.n");
111
112 return 0;
113 }
En nuestro programa hemos separado la definici´on del tipo struct Coleccion de la declara-
ci´on de la variable mis_cds. No es necesario. Podemos definir el tipo y declarar la variable en
una sola sentencia:
struct Coleccion {
134 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
struct CompactDisc cd[MAXDISCOS];
int cantidad;
} mis_cds ; // Declara la variable mis_cds como de tipo struct Coleccion.
Apuntemos ahora c´omo enriquecer nuestro programa de gesti´on de una colecci´on de discos
compactos almacenando, adem´as, las canciones de cada disco. Empezaremos por definir un
nuevo registro: el que modela una canci´on. De cada canci´on nos interesa el t´ıtulo, el autor y la
duraci´on:
1 struct Cancion {
2 char titulo[LONTITULO+1];
3 char autor[LONINTERPRETE+1];
4 struct Tiempo duracion;
5 };
Hemos de modificar el registro struct CompactDisc para que almacene hasta, digamos, 20
canciones:
1 #define MAXCANCIONES 20
2
3 struct CompactDisc {
4 char titulo[LONTITULO+1];
5 char interprete[LONINTERPRETE+1];
6 struct Tiempo duracion;
7 int anyo;
8 struct Cancion cancion[MAXCANCIONES]; // Vector de canciones.
9 int canciones; // N´umero de canciones que realmente hay.
10 };
¿C´omo leemos ahora un disco compacto? Aqu´ı tienes, convenientemente modificada, la por-
ci´on del programa que se encarga de ello:
1 ...
2 int main(void)
3 {
4 int segundos;
5 ...
6 switch(opcion) {
7 case Anyadir: // A˜nadir un CD.
8 if (mis_cds.cantidad == MAXDISCOS)
9 printf ("La base de datos est´a llena. Lo siento.n");
10 else {
11 printf ("T´ıtulo: ");
12 gets(mis_cds.cd[mis_cds.cantidad].titulo);
13 printf ("Int´erprete: ");
14 gets(mis_cds.cd[mis_cds.cantidad].interprete);
15 printf ("A~no: ");
16 gets(linea); sscanf (linea, "%d", &mis_cds.cd[mis_cds.cantidad].anyo);
17
18 do {
19 printf ("N´umero de canciones: ");
20 gets(linea); sscanf (linea, "%d", &mis_cds.cd[mis_cds.cantidad].canciones);
21 } while (mis_cds.cd[mis_cds.cantidad].canciones > MAXCANCIONES);
22
23 for (i=0; i<mis_cds.cd[mis_cds.cantidad].canciones; i++) {
24 printf ("T´ıtulo de la canci´on n´umero %d: ", i);
25 gets(mis_cds.cd[mis_cds.cantidad].cancion[i].titulo);
26 printf ("Autor de la canci´on n´umero %d: ", i);
27 gets(mis_cds.cd[mis_cds.cantidad].cancion[i].autor);
28 printf ("Minutos que dura la canci´on n´umero %d: ", i);
29 gets(linea);
30 sscanf (linea, "%d", &mis_cds.cd[mis_cds.cantidad].cancion[i].duracion.minutos);
31 printf ("y segundos: ");
32 gets(linea);
33 sscanf (linea, "%d", &mis_cds.cd[mis_cds.cantidad].cancion[i].duracion.segundos);
34 }
Introducci´on a la Programaci´on con C 135
2.5 Definici´on de nuevos tipos de datos
35
36 segundos = 0;
37 for (i=0; i<mis_cds.cd[mis_cds.cantidad].canciones; i++)
38 segundos +=60 * mis_cds.cd[mis_cds.cantidad].cancion[i].duracion.minutos
39 + mis_cds.cd[mis_cds.cantidad].cancion[i].duracion.segundos;
40 mis_cds.cd[mis_cds.cantidad].duracion.minutos = segundos / 60;
41 mis_cds.cd[mis_cds.cantidad].duracion.segundos = segundos % 60;
42
43 mis_cds.cantidad++;
44 }
45 break;
46 ...
47 }
Observa c´omo se calcula ahora la duraci´on del compacto como suma de las duraciones de todas
sus canciones.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 146 Dise˜na un programa C que gestione una agenda telef´onica. Cada entrada de la agenda
contiene el nombre de una persona y hasta 10 n´umeros de tel´efono. El programa permitir´a
a˜nadir nuevas entradas a la agenda y nuevos tel´efonos a una entrada ya existente. El men´u
del programa permitir´a, adem´as, borrar entradas de la agenda, borrar n´umeros de tel´efono
concretos de una entrada y efectuar b´usquedas por las primeras letras del nombre. (Si, por
ejemplo, tu agenda contiene entradas para ((Jos´e Mart´ınez)), ((Josefa P´erez)) y ((Jaime Primero)),
una b´usqueda por ((Jos)) mostrar´a a las dos primeras personas y una b´usqueda por ((J)) las
mostrar´a a todas.)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.5. Definici´on de nuevos tipos de datos
Los registros son nuevos tipos de datos cuyo nombre viene precedido por la palabra struct. C
permite definir nuevos nombres para los tipos existentes con la palabra clave typedef.
He aqu´ı un posible uso de typedef:
1 #define LONTITULO 80
2 #define LONINTERPRETE 40
3
4 struct Tiempo {
5 int minutos;
6 int segundos;
7 };
8
9 typedef struct Tiempo TipoTiempo;
10
11 struct Cancion {
12 char titulo[LONTITULO+1];
13 char autor[LONINTERPRETE+1];
14 TipoTiempo duracion;
15 };
16
17 typedef struct Cancion TipoCancion;
18
19
20 struct CompactDisc {
21 char titulo[LONTITULO+1];
22 char interprete[LONINTERPRETE+1];
23 TipoTiempo duracion;
24 int anyo;
25 TipoCancion cancion[MAXCANCIONES]; // Vector de canciones.
26 int canciones; // N´umero de canciones que realmente hay.
27 };
Hay una forma m´as compacta de definir un nuevo tipo a partir de un registro:
136 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros
1 #define LONTITULO 80
2 #define LONINTERPRETE 40
3
4 typedef struct {
5 int minutos;
6 int segundos;
7 } TipoTiempo;
8
9 typedef struct {
10 char titulo[LONTITULO+1];
11 char autor[LONINTERPRETE+1];
12 TipoTiempo duracion;
13 } TipoCancion;
14
15 typedef struct {
16 char titulo[LONTITULO+1];
17 char interprete[LONINTERPRETE+1];
18 TipoTiempo duracion;
19 int anyo;
20 TipoCancion cancion[MAXCANCIONES]; // Vector de canciones.
21 int canciones; // N´umero de canciones que realmente hay.
22 } TipoCompactDisc;
23
24 typedef struct {
25 TipoCompactDisc cd[MAXDISCOS];
26 int cds;
27 } TipoColeccion;
28
29 int main(void)
30 {
31 TipoColeccion mis_cds;
32 ...
Observa que, sistem´aticamente, hemos utilizado iniciales may´usculas para los nombres de
tipos de datos (definidos con typedef y struct o s´olo con struct). Es un buen convenio para
no confundir variables con tipos. Te recomendamos que hagas lo mismo o, en su defecto, que
adoptes cualquier otro criterio, pero que sea coherente.
El renombramiento de tipos no s´olo sirve para eliminar la molesta palabra clave struct,
tambi´en permite dise˜nar programas m´as legibles y en los que resulta m´as f´acil cambiar tipos
globalmente.
Imagina que en un programa nuestro representamos la edad de una persona con un valor
entre 0 y 127 (un char). Una variable edad se declarar´ıa as´ı:
char edad;
No es muy elegante: una edad no es un car´acter, sino un n´umero. Si definimos un ((nuevo)) tipo,
el programa es m´as legible:
typedef char TipoEdad;
TipoEdad edad;
Es m´as, si m´as adelante deseamos cambiar el tipo char por int, s´olo hemos de cambiar la l´ınea
que empieza por typedef, aunque hayamos definido decenas de variables del tipo TipoEdad:
typedef int TipoEdad;
TipoEdad edad;
Introducci´on a la Programaci´on con C 137
2.5 Definici´on de nuevos tipos de datos
Los cambios de tipos y sus consecuencias
Te hemos dicho que typedef permite definir nuevos tipos y facilita sustituir un tipo por
otro en diferentes versiones de un mismo programa. Es cierto, pero problem´atico. Imagina
que en una aplicaci´on definimos un tipo edad como un car´acter sin signo y que definimos
una variable de dicho tipo cuyo valor leemos de teclado:
1 #include <stdio.h>
2
3 typedef unsigned char TipoEdad;
4
5 int main(void)
6 {
7 TipoEdad mi_edad;
8
9 printf ("Introduzca edad: ");
10 scanf ("%hhu", &mi_edad);
11 printf ("Valor le´ıdo %hhun", mi_edad);
12
13 return 0;
14 }
¿Qu´e pasa si, posteriormente, decidimos que el tipo TipoEdad debiera ser un entero
de 32 bits? He aqu´ı una versi´on err´onea del programa:
1 #include <stdio.h>
2
3 typedef int TipoEdad;
4
5 int main(void)
6 {
7 TipoEdad mi_edad;
8
9 printf ("Introduzca edad: ");
10 scanf ("%hhu", &mi_edad); //
!
Mal!
11 printf ("Valor le´ıdo %hhun", mi_edad); //
!
Mal!
12
13 return 0;
14 }
¿Y por qu´e es err´oneo? Porque debi´eramos haber modificado adem´as las marcas de formato
de scanf y printf : en lugar de %hhu deber´ıamos usar ahora %d.
C no es un lenguaje id´oneo para este tipo de modificaciones. Otros lenguajes, como C++
soportan de forma mucho m´as flexible la posibilidad de cambiar tipos de datos, ya que no
obligan al programador a modificar un gran n´umero de l´ıneas del programa.
138 Introducci´on a la Programaci´on con C
Cap´ıtulo 3
Funciones
Un momento despu´es, Alicia atravesaba el cristal, y saltaba ´agilmente a la habitaci´on
del Espejo.
Lewis Carroll, Alicia a trav´es del espejo.
Vamos a estudiar la definici´on y uso de funciones en C. El concepto es el mismo que ya estudiaste
al aprender Python: una funci´on es un fragmento de programa parametrizado que efect´ua unos
c´alculos y, o devuelve un valor como resultado, o tiene efectos laterales (modificaci´on de variables
globales o argumentos, volcado de informaci´on en pantalla, etc.), o ambas cosas. La principal
diferencia entre Python y C estriba en el paso de par´ametros. En este aspecto, C presenta ciertas
limitaciones frente a Python, pero tambi´en ciertas ventajas. Entre las limitaciones tenemos la
necesidad de dar un tipo a cada par´ametro y al valor de retorno, y entre las ventajas, la
posibilidad de pasar variables escalares y modificar su valor en el cuerpo de la funci´on (gracias
al uso de punteros).
Estudiaremos tambi´en la posibilidad de declarar y usar variables locales, y volveremos a
tratar la recursividad. Adem´as, veremos c´omo implementar nuestros propios m´odulos mediante
las denominadas unidades de compilaci´on y la creaci´on de ficheros de cabecera.
Finalmente, estudiaremos la definici´on y el uso de macros, una especie de ((pseudo-funciones))
que gestiona el preprocesador de C.
3.1. Definici´on de funciones
En C no hay una palabra reservada (como def en Python) para iniciar la definici´on de una
funci´on. El aspecto de una definici´on de funci´on en C es ´este:
1 tipo_de_retorno identificador ( par´ametros )
2 {
3 cuerpo_de_la_funci´on
4 }
El cuerpo de la funci´on puede contener declaraciones de variables locales (t´ıpicamente en sus
primeras l´ıneas).
Aqu´ı tienes un ejemplo de definici´on de funci´on: una funci´on que calcula el logaritmo en
base b (para b entero) de un n´umero x. La hemos definido de un modo menos compacto de lo
que podemos hacer para ilustrar los diferentes elementos que puedes encontrar en una funci´on:
1 float logaritmo (float x, int b)
2 {
3 float logbase, resultado;
4
5 logbase = log10(b);
6 resultado = log10(x)/logbase;
7 return resultado;
8 }
Deteng´amonos a analizar brevemente cada uno de los componentes de la definici´on de una
funci´on e identifiqu´emoslos en el ejemplo:
Introducci´on a la Programaci´on con C 139
3.1 Definici´on de funciones
El tipo de retorno indica de qu´e tipo de datos es el valor devuelto por la funci´on como
resultado (m´as adelante veremos c´omo definir procedimientos, es decir, funciones sin valor
de retorno). Puedes considerar esto como una limitaci´on frente a Python: en C, cada
funci´on devuelve valores de un ´unico tipo. No podemos definir una funci´on que, seg´un
convenga, devuelva un entero, un flotante o una cadena, como hicimos en Python cuando
nos convino.
En nuestro ejemplo, la funci´on devuelve un valor de tipo float.
1 float logaritmo (float x, int b)
2 {
3 float logbase, resultado ;
4
5 logbase = log10(b);
6 resultado = log10(x)/logbase;
7 return resultado ;
8 }
El identificador es el nombre de la funci´on y, para estar bien formado, debe observar las
mismas reglas que se siguen para construir nombres de variables. Eso s´ı, no puedes definir
una funci´on con un identificador que ya hayas usado para una variable (u otra funci´on).
El identificador de nuestra funci´on de ejemplo es logaritmo:
1 float logaritmo (float x, int b)
2 {
3 float logbase, resultado;
4
5 logbase = log10(b);
6 resultado = log10(x)/logbase;
7 return resultado;
8 }
Entre par´entesis aparece una lista de declaraciones de par´ametros separadas por comas.
Cada declaraci´on de par´ametro indica tanto el tipo del mismo como su identificador1
.
Nuestra funci´on tiene dos par´ametros, uno de tipo float y otro de tipo int.
1 float logaritmo (float x, int b)
2 {
3 float logbase, resultado;
4
5 logbase = log10(b);
6 resultado = log10(x)/logbase;
7 return resultado;
8 }
El cuerpo de la funci´on debe ir encerrado entre llaves, aunque s´olo conste de una sentencia.
Puede empezar por una declaraci´on de variables locales a la que sigue una o m´as sentencias
C. La sentencia return permite finalizar la ejecuci´on de la funci´on y devolver un valor
(que debe ser del mismo tipo que el indicado como tipo de retorno). Si no hay sentencia
return, la ejecuci´on de la funci´on finaliza tambi´en al acabar de ejecutar la ´ultima de las
sentencias de su cuerpo, pero es un error no devolver nada con return si se ha declarado
la funci´on como tal, y no como procedimiento.
Nuestra funci´on de ejemplo tiene un cuerpo muy sencillo. Hay una declaraci´on de variables
(locales) y est´a formado por tres sentencias, dos de asignaci´on y una de devoluci´on de
valor:
1 float logaritmo (float x, int b)
2 {
3 float logbase, resultado;
4
1Eso en el caso de par´ametros escalares. Los par´ametros de tipo vectorial se estudiar´an m´as adelante.
140 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
5 logbase = log10(b);
6 resultado = log10(x)/logbase;
7 return resultado;
8 }
La sentencia (o sentencias) de devoluci´on de valor forma(n) parte del cuerpo y empieza(n)
con la palabra return. Una funci´on puede incluir m´as de una sentencia de devoluci´on de
valor, pero debes tener en cuenta que la ejecuci´on de la funci´on finaliza con la primera
ejecuci´on de una sentencia return.
1 float logaritmo (float x, int b)
2 {
3 float logbase, resultado;
4
5 logbase = log10(b);
6 resultado = log10(x)/logbase;
7 return resultado;
8 }
La funci´on logaritmo se invoca como una funci´on cualquiera de math.h:
logaritmo.c logaritmo.c
1 #include <stdio.h>
2 #include <math.h>
3
4 float logaritmo (float x, int b)
5 {
6 float logbase, resultado;
7
8 logbase = log10(b);
9 resultado = log10(x)/logbase;
10 return resultado;
11 }
12
13 int main (void)
14 {
15 float y;
16
17 y = logaritmo(128.0, 2);
18 printf ("%fn", y);
19
20 return 0;
21 }
Si ejecutamos el programa tenemos:
7.000000
Es necesario que toda funci´on se defina en el programa antes de la primera l´ınea en que
se usa. Por esta raz´on, todas nuestras funciones se definen delante de la funci´on main, que es
la funci´on que contiene el programa principal y a la que, por tanto, no se llama desde ning´un
punto del programa.2
Naturalmente, ha resultado necesario incluir la cabecera math.h en el programa, ya que
usamos la funci´on log10. Recuerda, adem´as, que al compilar se debe enlazar con la biblioteca
matem´atica, es decir, se debe usar la opci´on -lm de gcc.
Esta ilustraci´on te servir´a para identificar los diferentes elementos de la definici´on de una
funci´on y de su invocaci´on:
2Nuevamente hemos de matizar una afirmaci´on: en realidad s´olo es necesario que se haya declarado el prototipo
de la funci´on. M´as adelante daremos m´as detalles.
Introducci´on a la Programaci´on con C 141
3.1 Definici´on de funciones
float logaritmo (float x, int b)
{
float logbase, resultado;
logbase = log10(b);
resultado = log10(x)/logbase;
return resultado;
}
...
int main(void)
{
float y;
...
y = logaritmo( 128.0, 2 );
...
Par´ametros formales (o simplemente par´ametros)
Tipo de retorno
Identificador
Cabecera
Cuerpo
Declaraci´on de variables locales
Sentencia de devoluci´on de valor
Llamada, invocaci´on o activaci´on
Argumentos o par´ametros reales
Identificador
¡Ah! Te hemos dicho antes que la funci´on logaritmo no es muy compacta. Podr´ıamos haberla
definido as´ı:
float logaritmo (float x, int b)
{
return log10(x)/log10(b);
}
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 147 Define una funci´on que reciba un int y devuelva su cuadrado.
· 148 Define una funci´on que reciba un float y devuelva su cuadrado.
· 149 Define una funci´on que reciba dos float y devuelva 1 (((cierto))) si el primero es menor
que el segundo y 0 (((falso))) en caso contrario.
· 150 Define una funci´on que calcule el volumen de una esfera a partir de su radio r. (Recuerda
que el volumen de una esfera de radio r es 4/3πr3
.)
· 151 El seno hiperb´olico de x es
sinh =
ex
− e−x
2
.
Dise˜na una funci´on C que efect´ue el calculo de senos hiperb´olicos. (Recuerda que ex
se puede
calcular con la funci´on exp, disponible incluyendo math.h y enlazando el programa ejecutable
con la librer´ıa matem´atica.)
· 152 Dise˜na una funci´on que devuelva ((cierto)) (el valor 1) si el a˜no que se le suministra
como argumento es bisiesto, y ((falso)) (el valor 0) en caso contrario.
· 153 La distancia de un punto (x0, y0) a una recta Ax + By + C = 0 viene dada por
d =
Ax0 + By0 + C
√
A2 + B2
.
Dise˜na una funci´on que reciba los valores que definen una recta y los valores que definen un
punto y devuelva la distancia del punto a la recta.
142 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Veamos otro ejemplo de definici´on de funci´on:
1 int minimo(int a, int b, int c)
2 {
3 if (a <= b)
4 if (a <= c)
5 return a;
6 else
7 return c;
8 else
9 if (b <= c)
10 return b;
11 else
12 return c;
13 }
La funci´on minimo devuelve un dato de tipo int y recibe tres datos, tambi´en de tipo int. No
hay problema en que aparezca m´as de una sentencia return en una funci´on. El comportamiento
de return es el mismo que estudiamos en Python: tan pronto se ejecuta, finaliza la ejecuci´on
de la funci´on y se devuelve el valor indicado.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 154 Define una funci´on que, dada una letra min´uscula del alfabeto ingl´es, devuelva su
correspondiente letra may´uscula. Si el car´acter recibido como dato no es una letra min´uscula,
la funci´on la devolver´a inalterada.
· 155 ¿Qu´e error encuentras en esta funci´on?
1 int minimo (int a, b, c)
2 {
3 if (a <= b && a <= c)
4 return a;
5 if (b <= a && b <= c)
6 return b;
7 return c;
8 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Observa que main es una funci´on. Su cabecera es int main(void). ¿Qu´e significa void?
Significa que no hay par´ametros. Pero no nos adelantemos. En este mismo cap´ıtulo hablaremos
de funciones sin par´ametros.
3.2. Variables locales y globales
Cada funci´on puede definir sus propias variables locales defini´endolas en su cuerpo. C permite,
adem´as, definir variables fuera del cuerpo de cualquier funci´on: son las variables globales.
3.2.1. Variables locales
Las variables que declaramos justo al principio del cuerpo de una funci´on son variables locales.
Este programa, por ejemplo, declara dos variables locales para calcular el sumatorio
b
i=a i.
La variable local a sumatorio con identificador i nada tiene que ver con la variable del mismo
nombre que es local a main:
locales.c locales.c
1 #include <stdio.h>
2
3 int sumatorio(int a, int b)
4 {
5 int i, s; // Variables locales a sumatorio.
6
7 s = 0;
Introducci´on a la Programaci´on con C 143
3.2 Variables locales y globales
8 for (i=a; i<=b; i++)
9 s += i;
10 return s;
11 }
12
13 int main(void)
14 {
15 int i; // Variable local a main.
16
17 for (i=1; i<=10; i++)
18 printf ("Sumatorio de los %d primeros n´umeros naturales: %dn", i, sumatorio(1, i));
19 return 0;
20 }
Las variables locales i y s de sumatorio s´olo ((viven)) durante las llamadas a sumatorio.
La zona en la que es visible una variable es su ´ambito. Las variables locales s´olo son visibles
en el cuerpo de la funci´on en la que se declaran; ´ese es su ´ambito.
Variables locales a bloques
El concepto de variable local no est´a limitado, en C, a las funciones. En realidad, puedes
definir variables locales en cualquier bloque de un programa. F´ıjate en este ejemplo:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i;
6
7 for (i=0; i<3; i++) {
8 int j;
9 for (j=0; j<3; j++)
10 printf ("%d-%d ", i, j);
11 printf ("n");
12 }
13 return 0;
14 }
La variable j s´olo existe en el bloque en el que se ha declarado, es decir, en la zona sombreada.
Ese es su ´ambito. La variable i tiene un ´ambito que engloba al de j.
Puedes comprobar, pues, que una variable local a una funci´on es tambi´en una variable
local a un bloque: s´olo existe en el bloque que corresponde al cuerpo de la funci´on.
Como ya te dijimos en un cuadro del cap´ıtulo 1, C99 permite declarar variables de ´ındice
de bucle de usar y tirar. Su ´ambito se limita al bucle. Aqu´ı tienes un ejemplo en el que
hemos sombreado el ´ambito de la variable j:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i;
6
7 for (i=0; i<3; i++) {
8 for (int j=0; j<3; j++)
9 printf ("%d-%d ", i, j);
10 printf ("n");
11 }
12 return 0;
13 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 156 Dise˜na una funci´on que calcule el factorial de un entero n.
144 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
· 157 Dise˜na una funci´on que calcule xn
, para n entero y x de tipo float. (Recuerda que si
n es negativo, xn
es el resultado de multiplicar 1/x por s´ı mismo −n veces.)
· 158 El valor de la funci´on ex
puede aproximarse con el desarrollo de Taylor:
ex
≈ 1 + x +
x2
2!
+
x3
3!
+
x4
4!
+ · · ·
Dise˜na una funci´on que aproxime el valor de ex
usando n t´erminos del desarrollo de Taylor,
siendo n un n´umero entero positivo. (Puedes usar, si te conviene, la funci´on de exponenciaci´on
del ´ultimo ejercicio para calcular los distintos valores de xi
, aunque hay formas m´as eficientes
de calcular x/1!, x2
/2!, x3
/3!, . . . , ¿sabes c´omo? Plant´eate c´omo generar un t´ermino de la forma
xi
/i! a partir de un t´ermino de la forma xi−1
/(i − 1)!.)
· 159 El valor de la funci´on coseno puede aproximarse con el desarrollo de Taylor:
cos(x) ≈ 1 −
x2
2!
+
x4
4!
−
x6
6!
+ · · ·
Dise˜na una funci´on que aproxime el coseno de un valor x usando n t´erminos del desarrollo de
Taylor, siendo n un n´umero entero positivo.
· 160 Dise˜na una funci´on que diga si un n´umero es perfecto o no. Si el n´umero es perfecto,
devolver´a ((cierto)) (el valor 1) y si no, devolver´a ((falso)) (el valor 0). Un n´umero es perfecto si
es igual a la suma de todos sus divisores (excepto ´el mismo).
· 161 Dise˜na una funci´on que diga si un n´umero entero es o no es capic´ua.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.2.2. Variables globales
Las variables globales se declaran fuera del cuerpo de cualquier funci´on y son accesibles desde
cualquier punto del programa posterior a su declaraci´on. Este fragmento de programa, por
ejemplo, define una variable global i y una variable local a main con el mismo identificador:
globales.c globales.c
1 #include <stdio.h>
2
3 int i = 1; // Variable global i.
4
5 int doble(void)
6 {
7 i *= 2; // Referencia a la variable global i.
8 return i; // Referencia a la variable global i.
9 }
10
11 int main(void)
12 {
13 int i; // Variable local i.
14
15 for (i=0; i<5; i++) // Referencias a la variable local i.
16 printf ("%dn", doble()); // Ojo: el valor mostrado corresponde a la i global.
17
18 return 0;
19 }
F´ıjate en la p´erdida de legibilidad que supone el uso del identificador i en diferentes puntos
del programa: hemos de preguntarnos siempre si corresponde a la variable local o global. Te
desaconsejamos el uso generalizado de variables globales en tus programas. Como evitan usar
par´ametros en funciones, llegan a resultar muy c´omodas y es f´acil que abuses de ellas. No es
que siempre se usen mal, pero se requiere una cierta experiencia para formarse un criterio firme
que permita decidir cu´ando resulta conveniente usar una variable global y cu´ando conviene
suministrar informaci´on a funciones mediante par´ametros.
Como estudiante te pueden parecer un recurso c´omodo para evitar suministrar informaci´on
a las funciones mediante par´ametros. Ese peque˜no beneficio inmediato es, cr´eenos, un lastre a
Introducci´on a la Programaci´on con C 145
3.3 Funciones sin par´ametros
medio y largo plazo: aumentar´a la probabilidad de que cometas errores al intentar acceder o
modificar una variable y las funciones que definas en un programa ser´an dif´ıcilmente reutilizables
en otros. Est´as aprendiendo a programar y pretendemos evitar que adquieras ciertos vicios, as´ı
que te prohibimos que las uses. . . salvo cuando convenga que lo hagas.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 162 ¿Qu´e muestra por pantalla este programa?
contador global.c contador global.c
1 #include <stdio.h>
2
3 int contador ; // Variable global.
4
5 void fija(int a)
6 {
7 contador = a;
8 }
9
10 int decrementa(int a)
11 {
12 contador -= a;
13 return contador ;
14 }
15
16 void muestra(int contador)
17 {
18 printf ("[%d]n", contador);
19 }
20
21 void cuenta_atras(int a)
22 {
23 int contador;
24 for (contador=a; contador >=0; contador--)
25 printf ("%d ", contador);
26 printf ("n");
27 }
28
29 int main(void) {
30 int i;
31
32 contador = 10;
33 i = 1;
34 while (contador >= 0) {
35 muestra(contador);
36 cuenta_atras(contador);
37 muestra(i);
38 decrementa(i);
39 i *= 2;
40 }
41 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.3. Funciones sin par´ametros
Puedes definir una funci´on sin par´ametros dejando la palabra void como contenido de la lista
de par´ametros. Esta funci´on definida por nosotros, por ejemplo, utiliza la funci´on rand de
stdlib.h para devolver un n´umero aleatorio entre 1 y 6:
int dado (void)
{
return rand() % 6 + 1;
}
146 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
Para llamar a la funci´on dado hemos de a˜nadir un par de par´entesis a la derecha del iden-
tificador, aunque no tenga par´ametros.
Ya te hab´ıamos anticipado que la funci´on main es una funci´on sin par´ametros que devuelve
un entero:
dado.c dado.c
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int dado(void)
5 {
6 return rand() % 6 + 1;
7 }
8
9 int main(void)
10 {
11 int i;
12 for (i=0; i<10; i++)
13 printf ("%dn", dado() );
14 return 0;
15 }
((Calidad)) de los n´umeros aleatorios
La funci´on rand est´a pobremente implementada en muchas m´aquinas y genera patrones
repetitivos que hacen poco aleatoria la secuencia de n´umeros generada. Este problema se
agudiza si observamos los bits menos significativos de los n´umeros generados. . . ¡y eso es,
precisamente, lo que estamos haciendo con expresiones como rand() % 6! Una forma de
paliar este problema es usar una expresi´on diferente:
int dado(void)
{
return (int) ((double) rand() / ((double) RAND_MAX + 1) * 6) + 1;
}
La constante RAND_MAX es el mayor n´umero aleatorio que puede devolver rand. La divisi´on
hace que el n´umero generado est´e en el intervalo [0, 1[.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 163 El programa dado.c siempre genera la misma secuencia de n´umeros aleatorios. Para
evitarlo, debes proporcionar una semilla diferente con cada ejecuci´on del programa. El valor de
la semilla se suministra como ´unico argumento de la funci´on srand. Modifica dado.c para que
solicite al usuario la introducci´on del valor semilla.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Un uso t´ıpico de las funciones sin par´ametros es la lectura de datos por teclado que deben
satisfacer una serie de restricciones. Esta funci´on, por ejemplo, lee un n´umero entero de teclado
y se asegura de que sea par:
1 int lee_entero_par(void)
2 {
3 int numero;
4
5 scanf ("%d", &numero);
6 while (numero % 2 != 0) {
7 printf ("El n´umero debe ser par y %d no lo es.n", numero);
8 numero = scanf ("%d", &numero);
9 }
10 return numero;
11 }
Otro uso t´ıpico es la presentaci´on de men´us de usuario con lectura de la opci´on seleccionada
por el usuario:
Introducci´on a la Programaci´on con C 147
3.4 Procedimientos
1 int menu_principal(void)
2 {
3 int opcion;
4
5 do {
6 printf ("1) Alta usuarion");
7 printf ("2) Baja usuarion");
8 printf ("3) Consulta usuarion");
9 printf ("4) Salirn");
10
11 printf ("Opci´on: "); scanf ("%d", &opcion);
12 if (opcion < 1 || opcion > 4)
13 printf ("Opci´on no v´alida.n");
14 } while (opcion < 1 || opcion > 4);
15
16 return opcion;
17 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 164 Dise˜na una funci´on que lea por teclado un entero positivo y devuelva el valor le´ıdo.
Si el usuario introduce un n´umero negativo, la funci´on advertir´a del error por pantalla y leer´a
nuevamente el n´umero cuantas veces sea menester.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.4. Procedimientos
Un procedimiento, como recordar´as, es una funci´on que no devuelve valor alguno. Los procedi-
mientos provocan efectos laterales, como imprimir un mensaje por pantalla, modificar variables
globales o modificar el valor de sus par´ametros.
Los procedimientos C se declaran como funciones con tipo de retorno void. Mira este ejem-
plo:
1 #include <stdio.h>
2
3 void saludos(void)
4 {
5 printf ("Hola, mundo.n");
6 }
En un procedimiento puedes utilizar la sentencia return, pero sin devolver valor alguno.
Cuando se ejecuta una sentencia return, finaliza inmediatamente la ejecuci´on del procedimien-
to.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 165 Dise˜na un procedimiento que reciba un entero n y muestre por pantalla n asteriscos
seguidos con un salto de l´ınea al final.
· 166 Dise˜na un procedimiento que, dado un valor de n, dibuje con asteriscos un tri´angulo
rect´angulo cuyos catetos midan n caracteres. Si n es 5, por ejemplo, el procedimiento mostrar´a
por pantalla este texto:
1 *
2 **
3 ***
4 ****
5 *****
Puedes usar, si te conviene, el procedimiento desarrollado en el ejercicio anterior.
· 167 Dise˜na un procedimiento que reciba un n´umero entero entre 0 y 99 y muestre por
pantalla su trascripci´on escrita. Si le suministramos, por ejemplo, el valor 31, mostrar´a el texto
((treinta y uno)).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
148 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
3.5. Paso de par´ametros
3.5.1. Par´ametros escalares: paso por valor
Aunque modifiques el valor de un par´ametro escalar en una funci´on o procedimiento, el valor
de la variable pasada como argumento permanece inalterado. La funci´on bits del siguiente
programa, por ejemplo, modifica en su cuerpo el valor de su par´ametro num:
numbits.c numbits.c
1 #include <stdio.h>
2
3 int bits(unsigned int num )
4 {
5 int b = 0;
6
7 do {
8 b++;
9 num /= 2;
10 } while (num > 0);
11
12 return b;
13 }
14
15 int main(void)
16 {
17 unsigned int numero ;
18 int bitsnumero;
19
20 printf ("Introduce un entero positivo: "); scanf ("%u", &numero );
21 bitsnumero = bits(numero );
22 printf ("Hay %d bits en %u.n", bitsnumero, numero );
23 return 0;
24 }
Al ejecutar el programa y teclear el n´umero 128 se muestra por pantalla lo siguiente:
Introduce un entero positivo: 128
Hay 8 bits en 128.
Como puedes ver, el valor de numero permanece inalterado tras la llamada a bits, aunque
en el cuerpo de la funci´on se modifica el valor del par´ametro num (que toma el valor de numero
en la llamada). Un par´ametro es como una variable local, s´olo que su valor inicial se obtiene
copiando el valor del argumento que suministramos. As´ı pues, num no es numero, sino otra
variable que contiene una copia del valor de numero. Es lo que se denomina paso de par´ametro
por valor.
Llegados a este punto conviene que nos detengamos a estudiar c´omo se gestiona la memoria
en las llamadas a funci´on.
3.5.2. Organizaci´on de la memoria: la pila de llamadas a funci´on
En C las variables locales se gestionan, al igual que en Python, mediante una pila. Cada funci´on
activada se representa en la pila mediante un registro de activaci´on o trama de activaci´on. Se
trata de una zona de memoria en la que se almacenan las variables locales y par´ametros junto
a otra informaci´on, como el punto desde el que se llam´o a la funci´on.
Cuando iniciamos la ejecuci´on del programa numbits.c, se activa autom´aticamente la fun-
ci´on main. En ´ella tenemos dos variables locales: numero y bitsnumero.
main
numero
bitsnumero
Introducci´on a la Programaci´on con C 149
3.5 Paso de par´ametros
Si el usuario teclea el valor 128, ´este se almacena en numero:
main
128numero
bitsnumero
Cuando se produce la llamada a la funci´on bits, se crea una nueva trama de activaci´on:
llamada desde l´ınea 21
main
128numero
bitsnumero
bits
num
b
El par´ametro num recibe una copia del contenido de numero y se inicializa la variable local b
con el valor 0:
llamada desde l´ınea 21
main
128numero
bitsnumero
bits
128num
0b
Tras ejecutar el bucle de bits, la variable b vale 8. Observa que aunque num ha modificado su
valor y ´este proven´ıa originalmente de numero, el valor de numero no se altera:
llamada desde l´ınea 21
main
128numero
bitsnumero
bits
0num
8b
La trama de activaci´on de bits desaparece ahora, pero dejando constancia del valor devuelto
por la funci´on:
main
128numero
bitsnumero
8return
Y, finalmente, el valor devuelto se copia en bitsnumero:
main
128numero
8bitsnumero
150 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
Como ves, las variables locales s´olo ((viven)) durante la ejecuci´on de cada funci´on. C obtiene
una copia del valor de cada par´ametro y la deja en la pila. Cuando modificamos el valor de un
par´ametro en el cuerpo de la funci´on, estamos modificando el valor del argumento, no el de la
variable original.
Este otro programa declara numero como una variable global y trabaja directamente con
dicha variable:
numbits2.c numbits2.c
1 #include <stdio.h>
2
3 unsigned int numero;
4
5 int bits(void)
6 {
7 int b = 0;
8
9 do {
10 b++;
11 numero /= 2;
12 } while (numero > 0);
13
14 return b;
15 }
16
17 int main(void)
18 {
19 int bitsnumero;
20
21 printf ("Introduce un entero positivo: "); scanf ("%u", &numero );
22 bitsnumero = bits();
23 printf ("Hay %d bits, pero ahora ’numero’ vale %u.n", bitsnumero, numero);
24 return 0;
25 }
Las variables globales residen en una zona especial de la memoria y son accesibles desde cual-
quier funci´on. Representaremos dicha zona como un ´area enmarcada con una l´ınea discont´ınua.
Cuando se inicia la ejecuci´on del programa, ´esta es la situaci´on:
main bitsnumero
variables globales
numero
En main se da valor a la variable global numero:
main bitsnumero
variables globales
128numero
Y se llama a continuaci´on a bits sin argumento alguno:
bits
b
llamada desde l´ınea 22
main bitsnumero
variables globales
128numero
Introducci´on a la Programaci´on con C 151
3.5 Paso de par´ametros
El c´alculo de bits modifica el valor de numero. Tras la primera iteraci´on del bucle while, ´esta
es la situaci´on:
bits
1b
llamada desde l´ınea 22
main bitsnumero
variables globales
64numero
Cuando finaliza la ejecuci´on de bits tenemos:
8return
main bitsnumero
variables globales
0numero
Entonces se copia el valor devuelto en bitsnumero:
main 8bitsnumero
variables globales
0numero
El mensaje que obtenemos en pantalla es:
Introduce un entero positivo: 128
Hay 8 bits, pero ahora ’numero’ vale 0.
Bueno. Ahora sabes qu´e pasa con las variables globales y c´omo acceder a ellas desde las
funciones. Pero repetimos lo que te dijimos al aprender Python: pocas veces est´a justificado
acceder a variables globales, especialmente cuando est´as aprendiendo. Ev´ıtalas.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 168 Estudia este programa y muestra gr´aficamente el contenido de la memoria cuando se
van a ejecutar por primera vez las l´ıneas 24, 14 y 5.
suma cuadrados.c suma cuadrados.c
1 #include <stdio.h>
2
3 int cuadrado(int i)
4 {
5 return i * i;
6 }
7
8 int sumatorio(int a, int b)
9 {
10 int i, s;
11
12 s = 0;
13 for (i=a; i<=b; i++)
14 s += cuadrado(i);
15 return s;
16 }
17
18 int main(void)
19 {
20 int i, j;
21
22 i = 10;
152 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
23 j = 20;
24 printf ("%dn", sumatorio(i, j));
25 return 0;
26 }
· 169 Este programa muestra por pantalla los 10 primeros n´umeros primos. La funci´on si-
guiente genera cada vez un n´umero primo distinto. Gracias a la variable global ultimoprimo la
funci´on ((recuerda)) cu´al fue el ´ultimo n´umero primo generado. Haz una traza paso a paso del
programa (hasta que haya generado los 4 primeros primos). Muestra el estado de la pila y el
de la zona de variables globales en los instantes en que se llama a la funci´on siguienteprimo y
cuando ´esta devuelve su resultado
diez primos.c diez primos.c
1 #include <stdio.h>
2
3 int ultimoprimo = 0;
4
5 int siguienteprimo(void)
6 {
7 int esprimo , i;
8
9 do {
10 ultimoprimo++;
11 esprimo = 1;
12 for (i=2; i<ultimoprimo/2; i++)
13 if (ultimoprimo % i == 0) {
14 esprimo = 0;
15 break;
16 }
17 } while (!esprimo);
18 return ultimoprimo;
19 }
20
21 int main(void)
22 {
23 int i;
24
25 printf ("Los 10 primeros n´umeros primosn");
26 for (i=0; i<10; i++)
27 printf ("%dn", siguienteprimo());
28 return 0;
29 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
No hay problema con que las variables locales a una funci´on sean vectores. Su contenido
se almacena siempre en la pila. Este programa, por ejemplo, cuenta la cantidad de n´umeros
primos entre 1 y el valor que se le indique (siempre que no supere cierta constante N) con la
ayuda de la criba de Erat´ostenes. El vector con el que se efect´ua la criba es una variable local
a la funci´on que efect´ua el conteo:
eratostenes 1.c eratostenes.c
1 #include <stdio.h>
2
3 #define N 10
4
5 int cuenta_primos(int n) //Cuenta el n´umero de primos entre 1 y n.
6 {
7 char criba[N];
8 int i, j, numprimos;
9
10 /* Comprobemos que el argumento es v´alido */
11 if (n >= N)
12 return -1; // Devolvemos −1 si no es v´alido.
13
14 /* Inicializaci´on */
Introducci´on a la Programaci´on con C 153
3.5 Paso de par´ametros
15 criba[0] = 0;
16 for (i=1; i<n; i++)
17 criba[i] = 1;
18
19 /* Criba de Erat´ostenes */
20 for (i=2; i<n; i++)
21 if (criba[i])
22 for (j=2; i*j<n; j++)
23 criba[i*j] = 0;
24
25 /* Conteo de primos */
26 numprimos = 0;
27 for (i=0; i<n; i++)
28 if (criba[i])
29 numprimos++;
30
31 return numprimos;
32 }
33
34
35 int main(void)
36 {
37 int hasta, cantidad;
38
39 printf ("Introduce un valor:"); scanf ("%d", &hasta);
40 cantidad = cuenta_primos(hasta);
41 if (cantidad == -1)
42 printf ("No puedo efectuar ese c´alculo. El mayor valor permitido es %d.n", N-1);
43 else
44 printf ("Primos entre 1 y %d: %dn", hasta, cantidad);
45 return 0;
46 }
Cuando el programa inicia su ejecuci´on, se crea una trama de activaci´on en la que se albergan
las variables hasta y cantidad. Supongamos que cuando se solicita el valor de hasta el usuario
introduce el valor 6. He aqu´ı el aspecto de la memoria:
main
cantidad
6hasta
Se efect´ua entonces (l´ınea 40) la llamada a cuenta_primos, con lo que se crea una nueva tra-
ma de activaci´on. En ella se reserva memoria para todas las variables locales de cuenta_primos:
llamada desde l´ınea 40
main
cuenta primos
cantidad
6hasta
numprimos
i
j
criba
0 1 2 3 4 5 6 7 8 9
n
154 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
Observa que el vector criba ocupa memoria en la propia trama de activaci´on. Completa t´u
mismo el resto de acciones ejecutadas por el programa ayud´andote de una traza de la pila de
llamadas a funci´on con gr´aficos como los mostrados.
3.5.3. Vectores de longitud variable
Te hemos dicho en el apartado 2.1 que los vectores han de tener talla conocida en tiempo de
compilaci´on. Es hora de matizar esa afirmaci´on. Los vectores locales a una funci´on pueden
determinar su talla en tiempo de ejecuci´on. Veamos un ejemplo:
eratostenes 2.c eratostenes.c
1 #include <stdio.h>
2
3 int cuenta_primos(int n) //Cuenta el n´umero de primos entre 1 y n.
4 {
5 char criba[n];
6 int i, j, numprimos;
7
8 /* Inicializaci´on */
9 criba[0] = 0;
10 for (i=1; i<n; i++)
11 criba[i] = 1;
12
13 /* Criba de Erat´ostenes */
14 for (i=2; i<n; i++)
15 if (criba[i])
16 for (j=2; i*j<n; j++)
17 criba[i*j] = 0;
18
19 /* Conteo de primos */
20 numprimos = 0;
21 for (i=0; i<n; i++)
22 if (criba[i])
23 numprimos++;
24
25 return numprimos;
26 }
27
28
29 int main(void)
30 {
31 int hasta, cantidad;
32
33 printf ("Introduce un valor:"); scanf ("%d", &hasta);
34 cantidad = cuenta_primos(hasta);
35 printf ("Primos entre 1 y %d: %dn", hasta, cantidad);
36 return 0;
37 }
F´ıjate en c´omo hemos definido el vector criba: la talla no es un valor constante, sino n, un
par´ametro cuyo valor es desconocido hasta el momento en que se ejecute la funci´on. Esta es
una caracter´ıstica de C99 y supone una mejora interesante del lenguaje.
3.5.4. Par´ametros vectoriales: paso por referencia
Este programa ilustra el modo en que podemos declarar y pasar par´ametros vectoriales a una
funci´on:
pasa vector.c pasa vector.c
1 #include <stdio.h>
2
3 #define TALLA 3
Introducci´on a la Programaci´on con C 155
3.5 Paso de par´ametros
4
5 void incrementa(int a[])
6 {
7 int i;
8
9 for (i=0; i<TALLA; i++)
10 a[i]++;
11 }
12
13 int main(void)
14 {
15 int i, v[TALLA];
16
17
18 printf ("Al principio:n");
19 for (i=0; i<TALLA; i++) {
20 v[i] = i;
21 printf ("%d: %dn", i, v[i]);
22 }
23 incrementa(v);
24 printf ("Despu´es de llamar a incrementa:n");
25 for (i=0; i<TALLA; i++)
26 printf ("%d: %dn", i, v[i]);
27 return 0;
28 }
F´ıjate en c´omo se indica que el par´ametro a es un vector de enteros: a˜nadiendo un par de
corchetes a su identificador. En la l´ınea 23 pasamos a incrementa el vector v. ¿Qu´e ocurre
cuando modificamos componentes del par´ametro vectorial a en la l´ınea 10?
Si ejecutamos el programa obtenemos el siguiente texto en pantalla:
Al principio:
0: 0
1: 1
2: 2
Despu´es de llamar a incrementa:
0: 1
1: 2
2: 3
¡El contenido de v se ha modificado! Ocurre lo mismo que ocurr´ıa en Python: los vectores s´ı
modifican su contenido cuando se altera el contenido del respectivo par´ametro en las llamadas
a funci´on.
Cuando se pasa un par´ametro vectorial a una funci´on no se efect´ua una copia de su contenido
en la pila: s´olo se copia la referencia a la posici´on de memoria en la que empieza el vector.
¿Por qu´e? Por eficiencia: no es infrecuente que los programas manejen vectores de tama˜no
considerable; copiarlos cada vez en la pila supondr´ıa invertir una cantidad de tiempo que, para
vectores de tama˜no medio o grande, podr´ıa ralentizar dr´asticamente la ejecuci´on del programa.
La aproximaci´on adoptada por C hace que s´olo sea necesario copiar en la pila 4 bytes, que es
lo que ocupa una direcci´on de memoria. Y no importa cu´an grande o peque˜no sea un vector: la
direcci´on de su primer valor siempre ocupa 4 bytes.
Veamos gr´aficamente, pues, qu´e ocurre en diferentes instantes de la ejecuci´on del programa.
Justo antes de ejecutar la l´ınea 23 tenemos esta disposici´on de elementos en memoria:
main
3i
v 0
0
1
1
2
2
En el momento de ejecutar la l´ınea 10 por primera vez, en la funci´on incrementa, la memoria
presenta este aspecto:
156 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
main
3i
v 0
0
1
1
2
2
llamada desde l´ınea 23
incrementa
0i
a
¿Ves? El par´ametro a apunta a v. Los cambios sobre elementos del vector a que tienen lugar al
ejecutar la l´ınea 10 tienen efecto sobre los correspondientes elementos de v, as´ı que v refleja los
cambios que experimenta a. Tras ejecutar el bucle de incrementa, tenemos esta situaci´on:
main
3i
v 1
0
2
1
3
2
llamada desde l´ınea 23
incrementa
3i
a
Y una vez ha finalizado la ejecuci´on de incrementa, ´esta otra:
main
3i
v 1
0
2
1
3
2
¿Y qu´e ocurre cuando el vector es una variable global? Pues b´asicamente lo mismo: las refe-
rencias no tienen por qu´e ser direcciones de memoria de la pila. Este programa es b´asicamente
id´entico al anterior, s´olo que v es ahora una variable global:
pasa vector 1.c pasa vector.c
1 #include <stdio.h>
2
3 #define TALLA 3
4
5 int v[TALLA];
6
7 void incrementa(int a[])
8 {
9 int i;
10
11 for (i=0; i<TALLA; i++)
12 a[i]++;
13 }
14
15 int main(void)
16 {
17 int i;
18
19 printf ("Al principio:n");
20 for (i=0; i<TALLA; i++) {
Introducci´on a la Programaci´on con C 157
3.5 Paso de par´ametros
21 v[i] = i;
22 printf ("%d: %dn", i, v[i]);
23 }
24 incrementa(v);
25 printf ("Despu´es de llamar a incrementa:n");
26 for (i=0; i<TALLA; i++)
27 printf ("%d: %dn", i, v[i]);
28 return 0;
29 }
Analicemos qu´e ocurre en diferentes instantes de la ejecuci´on del programa. Justo antes de
ejecutar la l´ınea 24, existen las variables locales a main y las variables globales:
main 3i
variables globales
v 0
0
1
1
2
2
Al llamar a incrementa se suministra un puntero a la zona de memoria de variables globales,
pero no hay problema alguno: el par´ametro a es un puntero que apunta a esa direcci´on.
main 3i
llamada desde l´ınea 24
incrementa 0i
a
variables globales
v 0
0
1
1
2
2
Los cambios al contenido de a se manifiestan en v:
main 3i
llamada desde l´ınea 24
incrementa 3i
a
variables globales
v 1
0
2
1
3
2
Y una vez ha finalizado la ejecuci´on de incrementa, el contenido de v queda modificado:
main 3i
variables globales
v 1
0
2
1
3
2
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 170 Dise˜na un programa C que manipule polinomios de grado menor o igual que 10. Un
polinomio se representar´a con un vector de float de tama˜no 11. Si p es un vector que representa
un polinomio, p[i] es el coeficiente del t´ermino de grado i. Dise˜na un procedimiento suma con
el siguiente perfil:
void suma(float p[], float q[], float r[])
El procedimiento modificar´a r para que contenga el resultado de sumar los polinomios p y q.
· 171 Dise˜na una funci´on que, dada una cadena y un car´acter, diga cu´antas veces aparece el
car´acter en la cadena.
158 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Hemos visto c´omo pasar vectores a funciones. Has de ser consciente de que no hay forma de
saber cu´antos elementos tiene el vector dentro de una funci´on: f´ıjate en que no se indica cu´antos
elementos tiene un par´ametro vectorial. Si deseas utilizar el valor de la talla de un vector tienes
dos posibilidades:
1. saberlo de antemano,
2. o proporcionarlo como par´ametro adicional.
Estudiemos la primera alternativa. F´ıjate en este fragmento de programa:
pasa vector talla.c pasa vector talla.c
1 #include <stdio.h>
2
3 #define TALLA1 20
4 #define TALLA2 10
5
6 void inicializa(int z[])
7 {
8 int i;
9
10 for (i=0; i<TALLA1; i++)
11 z[i] = 0;
12 }
13
14 void imprime(int z[])
15 {
16 int i;
17
18 for (i=0; i<TALLA1; i++)
19 printf ("%d ", z[i]);
20 printf ("n");
21 }
22
23 int main(void)
24 {
25 int x[TALLA1];
26 int y[TALLA2];
27
28 inicializa(x);
29 inicializa(y); //
!
Ojo!
30
31 imprime(x);
32 imprime(y); //
!
Ojo!
33
34 return 0;
35 }
Siguiendo esta aproximaci´on, la funci´on inicializa s´olo se puede utilizar con vectores de int de
talla TALLA1, como x. No puedes llamar a inicializa con y: si lo haces (¡y C te deja hacerlo!)
cometer´as un error de acceso a memoria que no te est´a reservada, pues el bucle recorre TALLA1
componentes, aunque y s´olo tenga TALLA2. Ese error puede abortar la ejecuci´on del programa
o, peor a´un, no haci´endolo pero alterando la memoria de alg´un modo indefinido.
Este es el resultado obtenido en un ordenador concreto:
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
El programa no ha abortado su ejecuci´on, pero ha mostrado 20 valores del vector y, que
s´olo tiene 10.
¿C´omo podemos dise˜nar una funci´on que pueda trabajar tanto con el vector x como con
el vector y? Siguiendo la segunda aproximaci´on propuesta, es decir, pasando como par´ametro
adicional la talla del vector en cuesti´on:
Introducci´on a la Programaci´on con C 159
3.5 Paso de par´ametros
pasa vector talla 1.c pasa vector talla.c
1 #include <stdio.h>
2
3 #define TALLA1 20
4 #define TALLA2 10
5
6 void inicializa(int z[], int talla )
7 {
8 int i;
9
10 for (i=0; i<talla ; i++)
11 z[i] = 0;
12 }
13
14 void imprime(int z[], int talla )
15 {
16 int i;
17
18 for (i=0; i<talla ; i++)
19 printf ("%d ", z[i]);
20 printf ("n");
21 }
22
23
24 int main(void)
25 {
26 int x[TALLA1];
27 int y[TALLA2];
28
29 inicializa(x, TALLA1);
30 inicializa(y, TALLA2);
31
32 imprime(x, TALLA1);
33 imprime(y, TALLA2);
34
35 return 0;
36 }
Ahora puedes llamar a la funci´on inicializa con inicializa(x, TALLA1) o inicializa(y, TALLA2).
Lo mismo ocurre con imprime. El par´ametro talla toma el valor apropiado en cada caso porque
t´u se lo est´as pasando expl´ıcitamente.
´Este es el resultado de ejecutar el programa ahora:
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
Correcto.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 172 Dise˜na un procedimiento ordena que ordene un vector de enteros. El procedimiento
recibir´a como par´ametros un vector de enteros y un entero que indique el tama˜no del vector.
· 173 Dise˜na una funci´on que devuelva el m´aximo de un vector de enteros. El tama˜no del
vector se suministrar´a como par´ametro adicional.
· 174 Dise˜na una funci´on que diga si un vector de enteros es o no es pal´ındromo (devolviendo
1 o 0, respectivamente). El tama˜no del vector se suministrar´a como par´ametro adicional.
· 175 Dise˜na una funci´on que reciba dos vectores de enteros de id´entica talla y diga si son
iguales o no. El tama˜no de los dos vectores se suministrar´a como par´ametro adicional.
· 176 Dise˜na un procedimiento que reciba un vector de enteros y muestre todos sus com-
ponentes en pantalla. Cada componente se representar´a separado del siguiente con una coma.
El ´ultimo elemento ir´a seguido de un salto de l´ınea. La talla del vector se indicar´a con un
par´ametro adicional.
160 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
· 177 Dise˜na un procedimiento que reciba un vector de float y muestre todos sus componentes
en pantalla. Cada componente se representar´a separado del siguiente con una coma. Cada 6
componentes aparecer´a un salto de l´ınea. La talla del vector se indicar´a con un par´ametro
adicional.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.5.5. Par´ametros escalares: paso por referencia mediante punteros
C permite modificar el valor de variables escalares en una funci´on recurriendo a sus direcciones
de memoria. Analicemos el siguiente ejemplo:
referencia local.c referencia local.c
1 #include <stdio.h>
2
3 void incrementa(int * a)
4 {
5 *a += 1;
6 }
7
8 int main(void)
9 {
10 int b;
11
12 b = 1;
13 printf ("Al principio b vale %dn", b);
14 incrementa(&b);
15 printf ("Y al final vale %dn", b);
16 return 0;
17 }
Al ejecutarlo, aparece en pantalla el siguiente texto:
Al principio b vale 1
Y al final vale 2
Efectivamente, b ha modificado su valor tras la llamada a incrementa. Observa la forma en
que se ha declarado el ´unico par´ametro de incrementa: int * a. O sea, a es del tipo int *. Un
tipo de la forma ((tipo *)) significa ((puntero a valor de tipo tipo)). Tenemos, por tanto, que a
es un ((puntero a entero)). No le pasamos a la funci´on el valor de un entero, sino el valor de la
direcci´on de memoria en la que se encuentra un entero.
F´ıjate ahora en c´omo pasamos el argumento en la llamada a incrementa de la l´ınea 14, que
es de la forma incrementa(&b). Estamos pasando la direcci´on de memoria de b (que es lo que
proporciona el operador &) y no el valor de b. Todo correcto, ya que hemos dicho que la funci´on
espera la direcci´on de memoria de un entero.
Al principio de la ejecuci´on de incrementa tendremos esta situaci´on:
main 1b
llamada desde l´ınea 14
incrementa a
El par´ametro a es un puntero que apunta a b. F´ıjate ahora en la sentencia que incrementa
el valor apuntado por a (l´ınea 5):
*a += 1;
El asterisco que precede a a no indica ((multiplicaci´on)). Ese asterisco es un operador unario
que hace justo lo contrario que el operador &: dada una direcci´on de memoria, accede al valor
de la variable apuntada. (Recuerda que el operador & obten´ıa la direcci´on de memoria de una
Introducci´on a la Programaci´on con C 161
3.5 Paso de par´ametros
El & de los par´ametros de scanf
Ahora ya puedes entender bien por qu´e las variables escalares que suministramos a scanf
para leer su valor por teclado van precedidas por el operador &: como scanf debe modificar
su valor, ha de saber en qu´e direcci´on de memoria residen. No ocurre lo mismo cuando
vamos a leer una cadena, pero eso es porque el identificador de la variable ya es, en ese
caso, una direcci´on de memoria.
variable.) O sea, C interpreta *a como accede a la variable apuntada por a, que es b, as´ı que
*a += 1 equivale a b += 1 e incrementa el contenido de la variable b.
¿Qu´e pasar´ıa si en lugar de *a += 1 hubi´esemos escrito a += 1? Se hubiera incrementado la
direcci´on de memoria a la que apunta el puntero, nada m´as.
¿Y si hubi´esemos escrito a++? Lo mismo: hubi´esemos incrementado el valor de la direcci´on
almacenada en a. ¿Y *a++?, ¿funcionar´ıa? A primera vista dir´ıamos que s´ı, pero no funciona
como esperamos. El operador ++ postfijo tiene mayor nivel de precedencia que el operador
unario *, as´ı que *a++ (post)incrementa la direcci´on a y accede a su contenido, por ese ´orden.
Nuevamente habr´ıamos incrementado el valor de la direcci´on de memoria, y no su contenido.
Si quieres usar operadores de incremento/decremento, tendr´as que utilizar par´entesis para que
los operadores se apliquen en el orden deseado: (*a)++.
Naturalmente, no s´olo puedes acceder as´ı a variables locales, tambi´en las variables globales
son accesibles mediante punteros:
referencia global.c referencia global.c
1 #include <stdio.h>
2
3 int b; // Variable global.
4
5 void incrementa(int * a)
6 {
7 *a += 1;
8 }
9
10 int main(void)
11 {
12 b = 1;
13 printf ("Al principio b vale %dn", b);
14 incrementa(&b);
15 printf ("Y al final vale %dn", b);
16 return 0;
17 }
El aspecto de la memoria cuando empieza a ejecutarse la funci´on incrementa es ´este:
main
llamada desde l´ınea 14
incrementa a
variables globales
1b
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 178 Dise˜na un procedimiento que modifique el valor del par´ametro de tipo float para
que valga la inversa de su valor cuando ´este sea distinto de cero. Si el n´umero es cero, el
procedimiento dejar´a intacto el valor del par´ametro.
Si a vale 2.0, por ejemplo, inversa(&a) har´a que a valga 0.5.
· 179 Dise˜na un procedimiento que intercambie el valor de dos n´umeros enteros.
Si a y b valen 1 y 2, respectivamente, la llamada intercambia(&a, &b) har´a que a pase a
valer 2 y b pase a valer 1.
· 180 Dise˜na un procedimiento que intercambie el valor de dos n´umeros float.
162 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
La dualidad vector/puntero, el paso de vectores y el paso por referencia
Cuando pasamos un vector a una funci´on estamos pasando, realmente, una direcci´on de
memoria: aquella en la que empieza la zona de memoria reservada para el vector. Cuando
pasamos una variable escalar por referencia, tambi´en estamos pasando una direcci´on de
memoria: aquella en la que empieza la zona de memoria reservada para el valor escalar.
¿Qu´e diferencia hay entre una y otra direcci´on? Ninguna: un puntero siempre es un puntero.
F´ıjate en este programa:
dualidad.c dualidad.c
1 #include <stdio.h>
2
3 #define TALLA 10
4
5 void procedimiento(int *a, int b[])
6 {
7 printf ("%22d %6dn", *a, b[0]);
8 printf ("%22d %6dn", a[0], *b); //
!
Ojo!
9 }
10
11 int main(void)
12 {
13 int x[TALLA], i, y = 10;
14
15 for (i=0; i<TALLA; i++) x[i] = i + 1;
16 printf ("1) procedimiento( &y, x):n");
17 procedimiento(&y, x);
18 printf ("2) procedimiento( x, &y):n");
19 procedimiento(x, &y);
20 printf ("3) procedimiento(&x[0], &x[1]):n");
21 procedimiento(&x[0], &x[1]);
22
23 return 0;
24 }
Esta es la salida resultante de su ejecuci´on:
1) procedimiento( &y, x):
10 1
10 1
2) procedimiento( x, &y):
1 10
1 10
3) procedimiento(&x[0], &x[1]):
1 2
1 2
Observa qu´e ha ocurrido: en procedimiento se puede usar a y b como si fueran vectores
o variables escalares pasadas por referencia. Y podemos pasar a procedimiento tanto la
direcci´on de un vector de ints como la direcci´on de una variable escalar de tipo int.
La conclusi´on es clara: ((int * a)) e ((int b[])) son sin´onimos cuando se declara un
par´ametro, pues en ambos casos se describen punteros a direcciones de memoria en las
que residen sendos valores enteros (o donde empieza una serie de valores enteros).
Aunque sean expresiones sin´onimas y, por tanto, intercambiables, interesa que las uses
((correctamente)), pues as´ı mejorar´a la legibilidad de tus programas: usa int * cuando quieras
pasar la direcci´on de un entero y int [] cuando quieras pasar la direcci´on de un vector de
enteros.
· 181 Dise˜na un procedimiento que asigne a todos los elementos de un vector de enteros un
valor determinado. El procedimiento recibir´a tres datos: el vector, su n´umero de elementos y el
valor que que asignamos a todos los elementos del vector.
· 182 Dise˜na un procedimiento que intercambie el contenido completo de dos vectores de
enteros de igual talla. La talla se debe suministrar como par´ametro.
Introducci´on a la Programaci´on con C 163
3.5 Paso de par´ametros
En C s´olo hay paso por valor
Este apartado intenta que aprendas a distinguir el paso de par´ametros por valor y por
referencia. ¡Pero la realidad es que C s´olo tiene paso de par´ametros por valor! Cuando pasas
una referencia, est´as pasando expl´ıcitamente una direcci´on de memoria gracias al operador
&, y lo que hace C es copiar dicha direcci´on en la pila, es decir, pasa por valor una direcci´on
para simular el paso de par´ametros por referencia. La extra˜na forma de pasar el par´ametro
hace que tengas que usar el operador * cada vez que deseas acceder a ´el en el cuerpo de la
funci´on.
En otros lenguajes, como Pascal, es posible indicar que un par´ametro se pasa por referen-
cia sin que tengas que usar un operador (equivalente a) & al efectuar el paso o un operador
(equivalente a) * cuando usas el par´ametro en el cuerpo de la funci´on. Por ejemplo, este
programa Pascal incluye un procedimiento que modifica el valor de su par´ametro:
program referencia;
var b : integer;
procedure incrementa (var a : integer);
begin
a := a + 1;
end;
begin (* programa principal *)
b := 1;
writeln(’b val´ıa ’, b);
incrementa(b);
writeln(’b vale ’, b)
end.
C++ es una extensi´on de C que permite el paso de par´ametros por referencia. Usa para ello
el car´acter & en la declaraci´on del par´ametro:
1 #include <stdio.h>
2
3 void incrementa(int & a)
4 {
5 a += 1;
6 }
7
8 int main(void)
9 {
10 int b;
11
12 b = 1;
13 printf ("Al principio b vale %dn", b);
14 incrementa(b);
15 printf ("Y al final vale %dn", b);
16 return 0;
17 }
(Aunque no venga a cuento, observa lo diferente que es C de Pascal (y aun as´ı, lo semejante
que es) y c´omo el programa C++ presenta un aspecto muy semejante a uno equivalente
escrito en C.)
· 183 Dise˜na un procedimiento que asigne a un entero la suma de los elementos de un vector
de enteros. Tanto el entero (su direcci´on) como el vector se suministrar´an como par´ametros.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Un uso habitual del paso de par´ametros por referencia es la devoluci´on de m´as de un valor
como resultado de la ejecuci´on de una funci´on. Ve´amoslo con un ejemplo. Dise˜nemos una funci´on
que, dados un ´angulo α (en radianes) y un radio r, calcule el valor de x = r cos(α) e y = r sin(α):
164 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
x
yr
α
No podemos dise˜nar una funci´on que devuelva los dos valores. Hemos de dise˜nar un procedi-
miento que devuelva los valores resultantes como par´ametros pasados por referencia:
paso por referencia.c
1 #include <stdio.h>
2 #include <math.h>
3
4 void calcula_xy(float alfa, float radio, float * x, float * y)
5 {
6 *x = radio * cos(alfa);
7 *y = radio * sin(alfa);
8 }
¿Y c´omo llamamos al procedimiento? Aqu´ı tienes un ejemplo de uso:
paso por referencia.c paso por referencia.c
.
.
.
8 }
9
10 int main(void)
11 {
12 float r, angulo, horizontal, vertical;
13
14 printf ("Introduce el ´angulo (en radianes): "); scanf ("%f", &angulo);
15 printf ("Introduce el radio: "); scanf ("%f", &r);
16 calcula_xy(angulo, r, &horizontal , &vertical );
17 printf ("Resultado: (%f, %f)n", horizontal , vertical );
18
19 return 0;
20 }
¿Ves? Las variables horizontal y vertical no se inicializan en main: reciben valores como resul-
tado de la llamada a calcula_xy.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 184 Dise˜na una funci´on que calcule la inversa de calcula_xy, es decir, que obtenga el valor
del radio y del ´angulo a partir de x e y.
· 185 Dise˜na una funci´on que reciba dos n´umeros enteros a y b y devuelva, simult´aneamente,
el menor y el mayor de ambos. La funci´on tendr´a esta cabecera:
1 void minimax (int a, int b, int * min, int * max)
· 186 Dise˜na una funci´on que reciba un vector de enteros, su talla y un valor de tipo entero al
que denominamos buscado. La funci´on devolver´a (mediante return) el valor 1 si buscado tiene el
mismo valor que alg´un elemento del vector y 0 en caso contrario. La funci´on devolver´a, adem´as,
la distancia entre buscado y el elemento m´as pr´oximo a ´el.
La cabecera de la funci´on ha de ser similar a ´esta:
1 int busca(int vector[], int talla, int buscado, int * distancia)
Te ponemos un par de ejemplos para que veas qu´e debe hacer la funci´on.
1 #include <stdio.h>
2
3 #define TALLA 6
Introducci´on a la Programaci´on con C 165
3.5 Paso de par´ametros
4
5 // Define aqu´ı la funci´on
6 ...
7
8 int main(void)
9 {
10 int v[TALLA], distancia, encontrado, buscado, i;
11
12 for (i=0; i<TALLA; i++) {
13 printf ("Introduce el elemento %d: ", i);
14 scanf ("%d", &v[i]);
15 }
16
17 printf ("
?
Qu´e valor busco?: ");
18 scanf ("%d", &buscado);
19
20 encontrado = busca(v, TALLA, buscado, &distancia);
21 if (encontrado)
22 printf ("Encontr´e el valor %d.n", buscado);
23 else
24 printf ("No est´a. El elemento m´as pr´oximo est´a a distancia %d.n", distancia);
25
26 printf ("
?
Qu´e valor busco ahora?: ");
27 scanf ("%d", &buscado);
28
29 encontrado = busca(v, TALLA, buscado, &distancia);
30 if (encontrado)
31 printf ("Encontr´e el valor %d.n", buscado);
32 else
33 printf ("No est´a. El elemento m´as pr´oximo est´a a distancia %d.n", distancia);
34
35 return 0;
36 }
Al ejecutar el programa obtenemos esta salida por pantalla:
Introduce el elemento: 0
Introduce el elemento: 5
Introduce el elemento: 10
Introduce el elemento: 15
Introduce el elemento: 20
Introduce el elemento: 25
?
Qu´e valor busco?: 5
Encontr´e el valor 5.
?
Qu´e valor busco ahora?: 17
No est´a. El elemento m´as pr´oximo est´a a distancia 2.
· 187 Modifica la funci´on del ejercicio anterior para que, adem´as de la distancia al elemento
m´as pr´oximo, devuelva el valor del elemento m´as pr´oximo.
· 188 Modifica la funci´on del ejercicio anterior para que, adem´as de la distancia al elemento
m´as pr´oximo y el elemento m´as pr´oximo, devuelva el valor de su ´ındice.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.5.6. Paso de registros a funciones
No s´olo puedes pasar escalares y vectores como argumentos, tambi´en puedes pasar registros. El
paso de registros es por valor, o sea, copiando el contenido en la pila, a menos que t´u mismo
pases un puntero a su direcci´on de memoria.
Este programa, por ejemplo, define un tipo de datos para representar puntos en un espacio
de tres dimensiones y una funci´on que calcula la distancia de un punto al origen:
166 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
1 #include <stdio.h>
2 #include <math.h>
3
4 struct Punto {
5 float x, y, z;
6 };
7
8 float distancia(struct Punto p)
9 {
10 return sqrt( p.x*p.x + p.y*p.y + p.z*p.z );
11 }
12
13 int main(void)
14 {
15 struct Punto pto;
16
17 pto.x = 1;
18 pto.y = 1;
19 pto.z = 1;
20
21 printf ("Distancia al origen: %fn", distancia(pto));
22 return 0;
23 }
Al pasar un registro a la funci´on, C copia en la pila cada uno de los valores de sus campos. Ten
en cuenta que una variable de tipo struct Punto ocupa 24 bytes (contiene 3 valores de tipo
float). Variables de otros tipos registro que definas pueden ocupar cientos o incluso miles de
bytes, as´ı que ve con cuidado: llamar a una funci´on pasando registros por valor puede resultar
ineficiente. Por cierto, no es tan extra˜no que un registro ocupe cientos de bytes: uno o m´as de
sus campos podr´ıa ser un vector. Tambi´en en ese caso se estar´ıa copiando su contenido ´ıntegro
en la pila.
Eso s´ı, como est´as pasando una copia, las modificaciones del valor de un campo en el cuerpo
de la funci´on no tendr´an efectos perceptibles fuera de la funci´on.
Como te hemos anticipado, tambi´en puedes pasar registros por referencia. En tal caso s´olo
se estar´a copiando en la pila la direcci´on de memoria en la que empieza el registro (y eso son
4 bytes), mida lo que mida ´este. Se trata, pues, de un paso de par´ametros m´as eficiente. Eso
s´ı, has de tener en cuenta que los cambios que efect´ues a cualquier campo del par´ametro se
reflejar´an en el campo correspondiente de la variable que suministraste como argumento.
Esta funci´on, por ejemplo, define dos par´ametros: uno que se pasa por referencia y otro
que se pasa por valor. La funci´on traslada un punto p en el espacio (modificando los campos
del punto original) de acuerdo con el vector de desplazamiento que se indica con otro punto
(traslacion):
1 void traslada(struct Punto * p, struct Punto traslacion)
2 {
3 (*p).x += traslacion.x;
4 (*p).y += traslacion.y;
5 (*p).z += traslacion.z;
6 }
Observa c´omo hemos accedido a los campos de p. Ahora p es una direcci´on de memoria (es de
tipo struct Punto *), y *p es la variable apuntada por p (y por tanto, es de tipo struct Punto).
El campo x es accedido con (*p).x: primero se accede al contenido de la direcci´on de memoria
apuntada por p, y luego al campo x del registro *p, de ah´ı que usemos par´entesis.
Es tan frecuente la notaci´on (*p).x que existe una forma compacta equivalente:
1 void traslada(struct Punto * p, struct Punto traslacion)
2 {
3 p->x += traslacion.x;
4 p->y += traslacion.y;
5 p->z += traslacion.z;
6 }
La forma p->x es absolutamente equivalente a (*p).x.
Introducci´on a la Programaci´on con C 167
3.5 Paso de par´ametros
Recuerda, pues, que dentro de una funci´on se accede a los campos de forma distinta seg´un
se pase un valor por copia o por referencia:
1. con el operador punto, como en traslacion.x, si la variable se ha pasado por valor;
2. con el operador ((flecha)), como en p->x, si la variable se ha pasado por referencia (equi-
valentemente, puedes usar la notaci´on (*p).x).
Acabemos este apartado mostrando una rutina que pide al usuario que introduzca las coor-
denadas de un punto:
1 void lee_punto(struct Punto * p)
2 {
3 printf ("x: "); scanf ("%f", &p->x);
4 printf ("y: "); scanf ("%f", &p->y);
5 printf ("z: "); scanf ("%f", &p->z);
6 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 189 Este ejercicio y los siguientes de este bloque tienen por objeto construir una serie de
funciones que permitan efectuar transformaciones afines sobre puntos en el plano. Los puntos
ser´an variables de tipo struct Punto, que definimos as´ı:
1 struct Punto {
2 float x, y;
3 };
Dise˜na un procedimiento muestra_punto que muestre por pantalla un punto. Un punto p tal que
p.x vale 2.0 y p.y vale 0.2 se mostrar´a en pantalla as´ı: (2.000000, 0.200000). El procedimiento
muestra_punto recibir´a un punto por valor.
Dise˜na a continuaci´on un procedimiento que permita leer por teclado un punto. El procedi-
miento recibir´a por referencia el punto en el que se almacenar´an los valores le´ıdos.
· 190 La operaci´on de traslaci´on permite desplazar un punto de coordenadas (x, y) a (x+a, y+
b), siendo el desplazamiento (a, b) un vector (que representamos con otro punto). Implementa
una funci´on que reciba dos par´ametros de tipo punto y modifique el primero de modo que se
traslade lo que indique el vector.
· 191 La operaci´on de escalado transforma un punto (x, y) en otro (ax, ay), donde a es un
factor de escala (real). Implementa una funci´on que escale un punto de acuerdo con el factor
de escala a que se suministre como par´ametro (un float).
· 192 Si rotamos un punto (x, y) una cantidad de θ radianes alrededor del origen, obtenemos
el punto
(x cos θ − y sin θ, x sin θ + y cos θ).
Define una funci´on que rote un punto la cantidad de grados que se especifique.
· 193 La rotaci´on de un punto (x, y) una cantidad de θ radianes alrededor de un punto (a, b)
se puede efectuar con una traslaci´on con el vector (−a, −b), una rotaci´on de θ radianes con
respecto al origen y una nueva traslaci´on con el vector (a, b). Dise˜na una funci´on que permita
trasladar un punto un n´umero dado de grados alrededor de otro punto.
· 194 Dise˜na una funci´on que diga si dos puntos son iguales.
· 195 Hemos definido un tipo registro para representar complejos as´ı:
1 struct Complejo {
2 float real;
3 float imag;
4 };
Dise˜na e implementa los siguientes procedimientos para su manipulaci´on:
leer un complejo de teclado;
mostrar un complejo por pantalla;
168 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
el m´odulo de un complejo (|a + bi| =
√
a2 + b2);
el opuesto de un complejo (−(a + bi) = −a − bi);
el conjugado de un complejo (a + bi = a − bi);
la suma de dos complejos ((a + bi) + (c + di) = (a + c) + (b + d)i);
la diferencia de dos complejos ((a + bi) − (c + di) = (a − c) + (b − d)i);
el producto de dos complejos ((a + bi) · (c + di) = (ac − bd) + (ad + bc)i);
la divisi´on de dos complejos (a+bi
c+di = ac+bd
c2+d2 + bc−ad
c2+d2 i).
· 196 Define un tipo registro y una serie de funciones para representar y manipular fechas.
Una fecha consta de un d´ıa, un mes y un a˜no. Debes implementar funciones que permitan:
mostrar una fecha por pantalla con formato dd/mm/aaaa (por ejemplo, el 7 de junio de
2001 se muestra as´ı: 07/06/2001);
mostrar una fecha por pantalla como texto (por ejemplo, el 7 de junio de 2001 se muestra
as´ı: 7 de junio de 2001);
leer una fecha por teclado;
averiguar si una fecha cae en a˜no bisiesto;
averiguar si una fecha es anterior, igual o posterior a otra, devolviendo los valores −1, 0
o 1 respectivamente,
comprobar si una fecha existe (por ejemplo, el 29 de febrero de 2002 no existe):
calcular la diferencia de d´ıas entre dos fechas.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.5.7. Paso de matrices y otros vectores multidimensionales
El paso de vectores multidimensionales no es una simple extensi´on del paso de vectores unidi-
mensionales. Veamos. Aqu´ı tienes un programa incorrecto en el que se define una funci´on que
recibe una matriz y devuelve su elemento m´aximo:
pasa matriz mal.c E pasa matriz mal.c E
1 #include <stdio.h>
2
3 #define TALLA 3
4
5 int maximo(int a[][])
6 {
7 int i, j, m;
8
9 m = a[0][0];
10 for (i=0; i<TALLA; i++)
11 for (j=0; j<TALLA; j++)
12 if (a[i][j] > m)
13 m = a[i][j];
14
15 return m;
16 }
17
18 int main(void)
19 {
20 int matriz[TALLA][TALLA];
21 int i, j;
22
23 for (i=0; i<TALLA; i++)
Introducci´on a la Programaci´on con C 169
3.5 Paso de par´ametros
24 for (j=0; j<TALLA; j++)
25 matriz[i][j] = (i*j) % TALLA;
26
27 printf ("El m´aximo es %dn", maximo(matriz));
28 return 0;
29 }
El compilador no acepta ese programa. ¿Por qu´e? F´ıjate en la declaraci´on del par´ametro. ¿Qu´e
hay de malo? C no puede resolver los accesos de la forma a[i][j]. Si recuerdas, a[i][j] significa
((accede a la celda cuya direcci´on se obtiene sumando a la direcci´on a el valor i * COLUMNAS +
j)), donde COLUMNAS es el n´umero de columnas de la matriz a (en nuestro caso, ser´ıa TALLA).
Pero, ¿c´omo sabe la funci´on cu´antas columnas tiene a? ¡No hay forma de saberlo viendo una
definici´on del estilo int a[][]!
La versi´on correcta del programa debe indicar expl´ıcitamente cu´antas columnas tiene la
matriz. Hela aqu´ı:
pasa matriz.c pasa matriz.c
1 #include <stdio.h>
2
3 #define TALLA 3
4
5 int maximo(int a[][TALLA])
6 {
7 int i, j, m;
8
9 m = a[0][0];
10 for (i=0; i<TALLA; i++)
11 for (j=0; j<TALLA; j++)
12 if (a[i][j] > m)
13 m = a[i][j];
14
15 return m;
16 }
17
18 int main(void)
19 {
20 int matriz[TALLA][TALLA];
21 int i, j;
22
23 for (i=0; i<TALLA; i++)
24 for (j=0; j<TALLA; j++)
25 matriz[i][j] = (i*j) % TALLA;
26
27 printf ("El m´aximo es %dn", maximo(matriz));
28 return 0;
29 }
No ha sido necesario indicar cu´antas filas tiene la matriz (aunque somos libres de hacerlo). La
raz´on es sencilla: el n´umero de filas no hace falta para calcular la direcci´on en la que reside el
valor a[i][j].
As´ı pues, en general, es necesario indicar expl´ıcitamente el tama˜no de cada una de las
dimensiones del vector, excepto el de la primera (que puedes declarar o no, a voluntad). S´olo
as´ı obtiene C informaci´on suficiente para calcular los accesos a elementos del vector en el cuerpo
de la funci´on.
Una consecuencia de esta restricci´on es que no podremos definir funciones capaces de tra-
bajar con matrices de tama˜no arbitrario. Siempre hemos de definir expl´ıcitamente el tama˜no
de cada dimensi´on excepto de la primera. Habr´a una forma de superar este inconveniente, pero
tendremos que esperar al siguiente cap´ıtulo para poder estudiarla.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 197 Vamos a dise˜nar un programa capaz de jugar al tres en raya. El tablero se representar´a
con una matriz de 3 × 3. Las casillas ser´an caracteres. El espacio en blanco representar´a una
casilla vac´ıa; el car´acter ’o’ representar´a una casilla ocupada con un c´ırculo y el car´acter ’x’
representar´a una casilla marcada con una cruz.
170 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
Dise˜na una funci´on que muestre por pantalla el tablero.
Dise˜na una funci´on que detecte si el tablero est´a lleno.
Dise˜na una funci´on que detecte si alg´un jugador consigui´o hacer tres en raya.
Dise˜na una funci´on que solicite al usuario la jugada de los c´ırculos y modifique el tablero
adecuadamente. La funci´on debe detectar si la jugada es v´alida o no.
Dise˜na una funci´on que, dado un tablero, realice la jugada que corresponde a las cruces.
En una primera versi´on, haz que el ordenador ponga la cruz en la primera casilla libre.
Despu´es, modifica la funci´on para que el ordenador realice la jugada m´as inteligente.
Cuando hayas dise˜nado todas las funciones, monta un programa que las use y permita jugar al
tres en raya contra el computador.
· 198 El juego de la vida se juega sobre una matriz cuyas celdas pueden estar vivas o muertas.
La matriz se modifica a partir de su estado siguiendo unas sencilla reglas que tienen en cuenta
los, como mucho, 8 vecinos de cada casilla:
Si una celda viva est´a rodeada por 0 o 1 celdas vivas, muere de soledad.
Si una celda viva est´a rodeada por 4 celdas vivas, muere por superpoblaci´on.
Si una celda viva est´a rodeada por 2 o 3 celdas vivas, sigue viva.
Una celda muerta s´olo resucita si est´a rodeada por 3 celdas vivas.
Dise˜na una funci´on que reciba una matriz de 10 × 10 celdas en la que el valor 0 representa
((celda muerta)) y el valor 1 representa ((celda viva)). La funci´on modificar´a la matriz de acuerdo
con las reglas del juego de la vida. (Avisos: Necesitar´as una matriz auxiliar. Las celdas de los
bordes no tienen 8 vecinos, sino 3 o 5.)
A continuaci´on, monta un programa que permita al usuario introducir una disposici´on inicial
de celdas y ejecutar el juego de la vida durante n ciclos, siendo n un valor introducido por el
usuario.
Aqu´ı tienes un ejemplo de ((partida)) de 3 ciclos con una configuraci´on inicial curiosa:
Configuraci´on inicial:
__________
______xxx_
__________
__________
___xxx____
__xxx_____
__________
__________
__________
__________
Ciclos: 3
_______x__
_______x__
_______x__
____x_____
__x__x____
__x__x____
___x______
__________
__________
__________
__________
______xxx_
__________
__________
___xxx____
__xxx_____
Introducci´on a la Programaci´on con C 171
3.5 Paso de par´ametros
__________
__________
__________
__________
_______x__
_______x__
_______x__
____x_____
__x__x____
__x__x____
___x______
__________
__________
__________
· 199 Implementa el juego del buscaminas. El juego del buscaminas se juega en un tablero
de dimensiones dadas. Cada casilla del tablero puede contener una bomba o estar vac´ıa. Las
bombas se ubican aleatoriamente. El usuario debe descubrir todas las casillas que no contienen
bomba. Con cada jugada, el usuario descubre una casilla (a partir de sus coordenadas, un par
de letras). Si la casilla contiene una bomba, la partida finaliza con la derrota del usuario. Si la
casilla est´a libre, el usuario es informado de cu´antas bombas hay en las (como mucho) 8 casillas
vecinas.
Este tablero representa, en un terminal, el estado actual de una partida sobre un tablero de
8 × 8:
abcdefgh
a 00001___
b 00112___
c 222_____
d ________
e ____3___
f ________
g 1_111111
h __100000
Las casillas con un punto no han sido descubiertas a´un. Las casillas con un n´umero han sido
descubiertas y sus casillas vecinas contienen tantas bombas como se indica en el n´umero. Por
ejemplo, la casilla de coordenadas (’e’, ’e’) tiene 3 bombas en la vecindad y la casilla de
coordenadas (’b’, ’a’), ninguna.
Implementa un programa que permita seleccionar el nivel de dificultad y, una vez escogido,
genere un tablero y permita jugar con ´el al jugador.
Los niveles de dificultad son:
f´acil: tablero de 8 × 8 con 10 bombas.
medio: tablero de 15 × 15 con 40 bombas.
dif´ıcil: tablero de 20 × 20 con 100 bombas.
Debes dise˜nar funciones para desempe˜nar cada una de las acciones b´asicas de una partida:
dado un tablero y las coordenadas de una casilla, indicar si contiene bomba o no,
dado un tablero y las coordenadas de una casilla, devolver el n´umero de bombas vecinas,
dado un tablero y las coordenadas de una casilla, modificar el tablero para indicar que la
casilla en cuesti´on ya ha sido descubierta,
dado un tablero, mostrar su contenido en pantalla,
etc.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
172 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
3.5.8. Tipos de retorno v´alidos
Una funci´on puede devolver valores de cualquier tipo escalar o de registros, pero no puede
devolver vectores3
. La raz´on es simple: la asignaci´on funciona con valores escalares y registros,
pero no con vectores.
Ya hemos visto c´omo devolver valores escalares. A t´ıtulo ilustrativo te presentamos un
ejemplo de definici´on de registro y definici´on de funci´on que recibe como par´ametros un punto
(x, y) y un n´umero y devuelve un nuevo punto cuyo valor es (ax, ay):
1 struct Punto {
2 float x, y;
3 };
4
5 struct Punto escala(struct Punto p, float a)
6 {
7 struct Punto q;
8
9 q.x = a * p.x;
10 q.y = a * p.y;
11
12 return q;
13 }
Eso es todo. . . por el momento. Volveremos a la cuesti´on de si es posible devolver vectores
cuando estudiemos la gesti´on de memoria din´amica.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 200 Vuelve a implementar las funciones de manipulaci´on de puntos en el plano (ejerci-
cios 189–194) para que no modifiquen el valor del registro struct Punto que se suministra como
par´ametro. En su lugar, devolver´an el punto resultante como valor de retorno de la llamada a
funci´on.
· 201 Implementa nuevamente las funciones del ejercicio 195, pero devolviendo un nuevo
complejo con el resultado de operar con el complejo o complejos que se suministran como
par´ametros.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.5.9. Un ejercicio pr´actico: miniGalaxis
Pongamos en pr´actica lo aprendido dise˜nando una versi´on simplificada de un juego de rescate
espacial (Galaxis)4
al que denominaremos miniGalaxis.
MiniGalaxis se juega con un tablero de 9 filas y 20 columnas. En el tablero hay 5 n´aufragos
espaciales y nuestro objetivo es descubrir d´onde se encuentran. Contamos para ello con una
sonda espacial que podemos activar en cualquier casilla del tablero. La sonda dispara una
se˜nal en las cuatro direcciones cardinales que es devuelta por unos dispositivos que llevan los
n´aufragos. La sonda nos dice cu´antos n´aufragos espaciales han respondido, pero no desde qu´e
direcciones enviaron su se˜nal de respuesta. Cuando activamos la sonda en las coordenadas
exactas en las que se encuentra un na´ufrago, lo damos por rescatado. S´olo disponemos de 20
sondas para efectuar el rescate, as´ı que las hemos de emplear juiciosamente. De lo contrario, la
muerte de inocentes pesar´a sobre nuestra conciencia.
Lo mejor ser´a que te hagas una idea precisa del juego jugando. Al arrancar aparece esta
informaci´on en pantalla:
ABCDEFGHIJKLMNOPQRST
0 ++++++++++++++++++++
1 ++++++++++++++++++++
2 ++++++++++++++++++++
3 ++++++++++++++++++++
4 ++++++++++++++++++++
3Al menos no hasta que sepamos m´as de la gesti´on de memoria din´amica
4El nombre y la descripci´on puede que te hagan concebir demasiadas esperanzas: se trata de un juego muy
sencillito y falto de cualquier efecto especial. Galaxis fue concebido por Christian Franz y escrito para el Apple
Macintosh. M´as tarde, Eric Raymond lo reescribi´o para que fuera ejecutable en Unix.
Introducci´on a la Programaci´on con C 173
3.5 Paso de par´ametros
5 ++++++++++++++++++++
6 ++++++++++++++++++++
7 ++++++++++++++++++++
8 ++++++++++++++++++++
Hay 5 n´aufragos.
Dispones de 20 sondas.
Coordenadas:
El tablero se muestra como una serie de casillas. Arriba tienes letras para identificar las
columnas y a la izquierda n´umeros para las filas. El ordenador nos informa de que a´un quedan
5 n´aufragos por rescatar y que disponemos de 20 sondas. Se ha detenido mostrando el mensaje
((Coordenadas:)): est´a esperando a que digamos en qu´e coordenadas lanzamos una sonda. El
ordenador acepta una cadena que contenga un d´ıgito y una letra (en cualquier orden) y la letra
puede ser min´uscula o may´uscula. Lancemos nuestra primera sonda: escribamos 5b y pulsemos
la tecla de retorno de carro. He aqu´ı el resultado:
Coordenadas: 5b
ABCDEFGHIJKLMNOPQRST
0 +.++++++++++++++++++
1 +.++++++++++++++++++
2 +.++++++++++++++++++
3 +.++++++++++++++++++
4 +.++++++++++++++++++
5 .0..................
6 +.++++++++++++++++++
7 +.++++++++++++++++++
8 +.++++++++++++++++++
Hay 5 n´aufragos.
Dispones de 19 sondas.
Coordenadas:
El tablero se ha redibujado y muestra el resultado de lanzar la sonda. En la casilla de
coordenadas 5b aparece un cero: es el n´umero de na´ufragos que hemos detectado con la sonda.
Mala suerte. Las casillas que ahora aparecen con un punto son las exploradas por la sonda.
Ahora sabes que en ninguna de ellas hay un n´aufrago. Sigamos jugando: probemos con las
coordenadas 3I. Aqu´ı tienes la respuesta del ordenador:
Coordenadas: 3I
ABCDEFGHIJKLMNOPQRST
0 +.++++++.+++++++++++
1 +.++++++.+++++++++++
2 +.++++++.+++++++++++
3 ........1...........
4 +.++++++.+++++++++++
5 .0..................
6 +.++++++.+++++++++++
7 +.++++++.+++++++++++
8 +.++++++.+++++++++++
Hay 5 n´aufragos.
Dispones de 18 sondas.
Coordenadas:
En la casilla de coordenadas 3I aparece un uno: la sonda ha detectado la presencia de un
n´aufrago en alguna de las 4 direcciones. Sigamos. Probemos en 0I:
Coordenadas: i0
ABCDEFGHIJKLMNOPQRST
0 ........2...........
1 +.++++++.+++++++++++
2 +.++++++.+++++++++++
3 ........1...........
4 +.++++++.+++++++++++
5 .0..................
6 +.++++++.+++++++++++
174 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
7 +.++++++.+++++++++++
8 +.++++++.+++++++++++
Hay 5 n´aufragos.
Dispones de 17 sondas.
Coordenadas:
Dos n´aufragos detectados. Parece probable que uno de ellos est´e en la columna I. Lancemos
otra sonda en esa columna. Probemos con 2I:
Coordenadas: 2I
ABCDEFGHIJKLMNOPQRST
0 ........2...........
1 +.++++++.+++++++++++
2 ........X...........
3 ........1...........
4 +.++++++.+++++++++++
5 .0..................
6 +.++++++.+++++++++++
7 +.++++++.+++++++++++
8 +.++++++.+++++++++++
Hay 4 n´aufragos.
Dispones de 16 sondas.
Coordenadas:
¡Bravo! Hemos encontrado a uno de los n´aufragos. En el tablero se muestra con una X. Ya
s´olo quedan 4.
Bueno. Con esta partida inacabada puedes hacerte una idea detallada del juego. Dise˜nemos
el programa.
Empezamos por definir las estructuras de datos. La primera de ellas, el tablero de juego, que
es una simple matriz de 9 × 20 casillas. Nos vendr´a bien disponer de constantes que almacenen
el n´umero de filas y columnas para usarlas en la definici´on de la matriz:
1 #include <stdio.h>
2
3 #define FILAS 9
4 #define COLUMNAS 20
5
6 int main(void)
7 {
8 char espacio[FILAS][COLUMNAS];
9
10 return 0;
11 }
La matriz espacio es una matriz de caracteres. Hemos de inicializarla con caracteres ’+’, que
indican que no se han explorado sus casillas. En lugar de inicializarla en main, vamos a dise˜nar
una funci´on especial para ello. ¿Por qu´e? Para mantener main razonablemente peque˜no y
mejorar as´ı la legibilidad. A estas alturas no debe asustarnos definir funciones para las diferentes
tareas.
1 #include <stdio.h>
2
3 #define FILAS 9
4 #define COLUMNAS 20
5
6 #define NO_SONDEADA ’+’
7
8 void inicializa_tablero(char tablero[][COLUMNAS])
9 /* Inicializa el tablero de juego marcando todas las casillas como no sondeadas. */
10 {
11 int i, j;
12
13 for (i=0; i<FILAS; i++)
14 for (j=0; j<COLUMNAS; j++)
15 tablero[i][j] = NO_SONDEADA;
Introducci´on a la Programaci´on con C 175
3.5 Paso de par´ametros
16 }
17
18 int main(void)
19 {
20 char espacio[FILAS][COLUMNAS];
21
22 inicializa_tablero(espacio);
23
24 return 0;
25 }
Pasamos la matriz indicando el n´umero de columnas de la misma.5
En el interior de la funci´on
se modifica el contenido de la matriz. Los cambios afectar´an a la variable que suministremos
como argumento, pues las matrices se pasan siempre por referencia.
Hemos de mostrar por pantalla el contenido de la matriz en m´as de una ocasi´on. Podemos
dise˜nar un procedimiento que se encargue de esta tarea:
1 #include <stdio.h>
2
3 #define FILAS 9
4 #define COLUMNAS 20
5
6 #define NO_SONDEADA ’+’
7
8 ...
9
10 void muestra_tablero(char tablero[][COLUMNAS])
11 /* Muestra en pantalla el tablero de juego. */
12 {
13 int i, j;
14
15 // Etiquetar con una letra cada columna.
16 printf (" ");
17 for (j=0; j<COLUMNAS; j++) printf ("%c", ’A’+j);
18 printf ("n");
19
20 for (i=0; i<FILAS; i++) {
21 printf ("%d ", i); // Etiqueta de cada fila.
22 for (j=0; j<COLUMNAS; j++)
23 printf ("%c", tablero[i][j]);
24 printf ("n");
25 }
26 }
27
28 int main(void)
29 {
30 char espacio[FILAS][COLUMNAS];
31
32 inicializa_tablero(espacio);
33 muestra_tablero(espacio);
34
35 return 0;
36 }
El procedimiento muestra_tablero imprime, adem´as, del contenido del tablero, el nombre de
las columnas y el n´umero de las filas.
Por cierto, hay una discrepancia entre el modo con que nos referimos a las casillas (mediante
un d´ıgito y una letra) y el modo con el que lo hace el programa (mediante dos n´umeros enteros).
Cuando pidamos unas coordenadas al usuario lo haremos con una sentencia como ´esta:
5No hemos usado el nombre espacio, sino tablero, con el ´unico objetivo de resaltar que el par´ametro puede ser
cualquier matriz (siempre que su dimensi´on se ajuste a lo esperado), aunque nosotros s´olo usaremos la matriz
espacio como argumento. Si hubi´esemos usado el mismo nombre, es probable que hubi´esemos alimentado la
confusi´on entre par´ametros y argumentos que experiment´ais algunos.
176 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
1 ...
2 #define TALLACAD 80
3 ...
4 int main(void)
5 {
6 ...
7 char coordenadas[TALLACAD+1];
8
9 ...
10
11 printf ("Coordenadas: "); scanf ("%s", coordenadas);
12 ...
Como ves, las coordenadas se leer´an en una cadena. Nos convendr´a disponer, pues, de una
funci´on que ((traduzca)) esa cadena a un par de n´umeros y otra que haga lo contrario:
1 void de_fila_y_columna_a_numero_y_letra(int fila, int columna, char * coordenadas)
2 /* Convierte una fila y columna descritas num´ericamente en una fila y columna descritas
3 * como una cadena con un d´ıgito y una letra.
4 */
6 {
7 coordenadas[0] = ’0’ + fila;
8 coordenadas[1] = ’A’ + columna;
9 coordenadas[2] = ’0’;
10 }
11
12 int de_numero_y_letra_a_fila_y_columna(char coordenadas[], int * fila, int * columna)
13 /* Convierte una fila y columna con un d´ıgito y una letra (min´uscula o may´uscula) en
14 * cualquier orden a una fila y columna descritas num´ericamente.
15 */
17 {
18 if (strlen(coordenadas) != 2)
19 return 0;
20 if (coordenadas[0] >= ’0’ && coordenadas[0] <= ’8’ && isalpha(coordenadas[1])) {
21 *fila = coordenadas[0] - ’0’;
22 *columna = toupper(coordenadas[1]) - ’A’;
23 return 1;
24 }
25 if (coordenadas[1] >= ’0’ && coordenadas[1] <= ’8’ && isalpha(coordenadas[0])) {
26 *columna = toupper(coordenadas[0]) - ’A’;
27 *fila = coordenadas[1] - ’0’;
28 return 1;
29 }
30 return 0;
31 }
La primera funci´on (de_fila_y_columna_a_numero_y_letra) es muy sencilla: recibe el valor de la
fila y el valor de la columna y modifica el contenido de un puntero a una cadena. Observa que
es responsabilidad nuestra terminar correctamente la cadena coordenadas. La segunda funci´on
es algo m´as complicada. Una raz´on para ello es que efect´ua cierto tratamiento de errores. ¿Por
qu´e? Porque la cadena coordenadas ha sido introducida por el usuario y puede contener errores.
Usamos un convenio muy frecuente en los programas C:
Los valores se devuelven en la funci´on mediante par´ametros pasados por referencia,
y la funci´on devuelve un valor que indica si se detect´o o no un error (devuelve 0 si hubo
error, y 1 en caso contrario).
De este modo es posible invocar a la funci´on cuando leemos el contenido de la cadena de esta
forma:
1 ...
2 printf ("Coordenadas: "); scanf ("%s", coordenadas);
3 while (!de_numero_y_letra_a_fila_y_columna(coordenadas, &fila, &columna)) {
4 printf ("Coordenadas no v´alidas. Int´entelo de nuevo.nCoordenadas: ");
Introducci´on a la Programaci´on con C 177
3.5 Paso de par´ametros
5 scanf ("%s", coordenadas);
6 }
7 ...
Sigamos. Hemos de disponer ahora 5 n´aufragos en el tablero de juego. Podr´ıamos ponerlos
directamente en la matriz espacio modificando el valor de las casillas pertinentes, pero en tal caso
muestra_tablero los mostrar´ıa, revelando el secreto de su posici´on y reduciendo notablemente el
inter´es del juego ;-). ¿Qu´e hacer? Una posibilidad consiste en usar una matriz adicional en la
que poder disponer los n´aufragos. Esta nueva matriz no se mostrar´ıa nunca al usuario y ser´ıa
consultada por el programa cuando se necesitara saber si hay un n´aufrago en alguna posici´on
determinada del tablero. Si bien es una posibilidad interesante (y te la propondremos m´as
adelante como ejercicio), nos decantamos por seguir una diferente que nos permitir´a practicar
el paso de registros a funciones. Definiremos los siguientes registros:
...
#define MAX_NAUFRAGOS 5
struct Naufrago {
int fila, columna; // Coordenadas
int encontrado; //
?
Ha sido encontrado ya?
};
struct GrupoNaufragos {
struct Naufrago naufrago[MAX_NAUFRAGOS];
int cantidad;
};
...
El tipo registro struct Naufrago mantiene la posici´on de un n´aufrago y permite saber si sigue
perdido o si, por el contrario, ya ha sido encontrado. El tipo registro struct GrupoNaufragos
mantiene un vector de n´aufragos de talla MAX_NAUFRAGOS. Aunque el juego indica que hemos de
trabajar con 5 n´aufragos, usaremos un campo adicional con la cantidad de n´aufragos realmente
almacenados en el vector. De ese modo resultar´a sencillo modificar el juego (como te propone-
mos en los ejercicios al final de esta secci´on) para que se juegue con un n´umero de n´aufragos
seleccionado por el usuario.
Guardaremos los n´aufragos en una variable de tipo struct GrupoNaufragos:
1 ...
2
3 int main(void)
4 {
5 char espacio[FILAS][COLUMNAS];
6 struct GrupoNaufragos losNaufragos;
7
8 inicializa_tablero(espacio);
9 muestra_tablero(espacio);
10
11 return 0;
12 }
El programa deber´ıa empezar realmente por inicializar el registro losNaufragos ubicando a cada
n´aufrago en una posici´on aletoria del tablero. Esta funci´on (err´onea) se encarga de ello:
...
#include <stdlib.h>
...
void pon_naufragos(struct GrupoNaufragos * grupoNaufragos, int cantidad)
/* Situa aleatoriamente cantidad n´aufragos en la estructura grupoNaufragos. */
/* PERO LO HACE MAL. */
{
int fila, columna;
grupoNaufragos->cantidad = 0;
178 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
while (grupoNaufragos->cantidad != cantidad) {
fila = rand() % FILAS;
columna = rand() % COLUMNAS;
grupoNaufragos->naufrago[grupoNaufragos->cantidad].fila = fila;
grupoNaufragos->naufrago[grupoNaufragos->cantidad].columna = columna;
grupoNaufragos->naufrago[grupoNaufragos->cantidad].encontrado = 0;
grupoNaufragos->cantidad++;
}
}
¿Por qu´e est´a mal? Primero hemos de entenderla bien. Analic´emosla paso a paso. Empecemos
por la cabecera: la funci´on tiene dos par´ametros, uno que es una referencia (un puntero) a un
registro de tipo struct GrupoNaufragos y un entero que nos indica cu´antos n´aufragos hemos
de poner al azar. La rutina empieza inicializando a cero la cantidad de n´aufragos ya dispuestos
mediante una l´ınea como ´esta:
grupoNaufragos ->cantidad = 0;
¿Entiendes por qu´e se usa el operador flecha?: la variable grupoNaufragos es un puntero, as´ı que
hemos de acceder a la informaci´on apuntada antes de acceder al campo cantidad. Podr´ıamos
haber escrito esa misma l´ınea as´ı:
(*grupoNaufragos ).cantidad = 0;
pero hubiera resultado m´as inc´omodo (e ilegible). A continuaci´on, la funci´on repite cantidad
veces la acci´on consistente en seleccionar una fila y columna al azar (mediante la funci´on rand
de stdlib.h) y lo anota en una posici´on del vector de n´aufragos. Puede que esta l´ınea te resulte
un tanto dif´ıcil de entender:
grupoNaufragos->naufrago[grupoNaufragos->cantidad ].fila = fila;
pero no lo es tanto si la analizas paso a paso. Veamos. Empecemos por el ´ındice que hemos
sombreado arriba. La primera vez, es 0, la segunda 1, y as´ı sucesivamente. En aras de comprender
la sentencia, nos conviene reescribir la sentencia poniendo de momento un 0 en el ´ındice:
grupoNaufragos->naufrago[0].fila = fila;
M´as claro, ¿no? Piensa que grupoNaufragos->naufrago es un vector como cualquier otro, as´ı
que la expresi´on grupoNaufragos->naufrago[0] accede a su primer elemento. ¿De qu´e tipo es
ese elemento? De tipo struct Naufrago. Un elemento de ese tipo tiene un campo fila y se
accede a ´el con el operador punto. O sea, esa sentencia asigna el valor de fila al campo fila
de un elemento del vector naufrago del registro que es apuntado por grupoNaufragos. El resto
de la funci´on te debe resultar f´acil de leer ahora. Volvamos a la cuesti´on principal: ¿por qu´e
est´a mal dise˜nada esa funci´on? F´acil: porque puede ubicar dos n´aufragos en la misma casilla
del tablero. ¿C´omo corregimos el problema? Asegur´andonos de que cada n´aufrago ocupa una
casilla diferente. Tenemos dos posibilidades:
Generar las posiciones de cinco n´aufragos al azar y comprobar que son todas diferentes
entre s´ı. Si lo son, perfecto: hemos acabado; si no, volvemos a repetir todo el proceso.
Ir generando la posici´on de cada n´aufrago de una en una, comprobando cada vez que
´esta es distinta de la de todos los n´aufragos anteriores. Si no lo es, volvemos a generar la
posici´on de este n´aufrago concreto; si lo es, pasamos al siguiente.
La segunda resulta m´as sencilla de implementar y es, a la vez, m´as eficiente. Aqu´ı la tienes
implementada:
void pon_naufragos(struct GrupoNaufragos * grupoNaufragos, int cantidad)
/* Sit´ua aleatoriamente cantidad n´aufragos en la estructura grupoNaufragos. */
{
int fila, columna, ya_hay_uno_ahi, i;
grupoNaufragos->cantidad = 0;
while (grupoNaufragos->cantidad != cantidad) {
fila = rand() % FILAS;
columna = rand() % COLUMNAS;
ya_hay_uno_ahi = 0;
Introducci´on a la Programaci´on con C 179
3.5 Paso de par´ametros
for (i=0; i<grupoNaufragos->cantidad; i++)
if (fila == grupoNaufragos->naufrago[i].fila &&
columna == grupoNaufragos->naufrago[i].columna) {
ya_hay_uno_ahi = 1;
break;
}
if (!ya_hay_uno_ahi) {
grupoNaufragos->naufrago[grupoNaufragos->cantidad].fila = fila;
grupoNaufragos->naufrago[grupoNaufragos->cantidad].columna = columna;
grupoNaufragos->naufrago[grupoNaufragos->cantidad].encontrado = 0;
grupoNaufragos->cantidad++;
}
}
}
Nos vendr´a bien disponer de una funci´on que muestre por pantalla la ubicaci´on y estado de
cada n´aufrago. Esta funci´on no resulta ´util para el juego (pues perder´ıa toda la gracia), pero
s´ı para ayudarnos a depurar el programa. Podr´ıamos, por ejemplo, ayudarnos con llamadas a
esa funci´on mientras jugamos partidas de prueba y, una vez dado por bueno el programa, no
llamarla m´as. En cualquier caso, aqu´ı la tienes:
void muestra_naufragos(struct GrupoNaufragos grupoNaufragos)
/* Muestra por pantalla las coordenadas de cada n´aufrago e informa de si sigue perdido.
* ´Util para depuraci´on del programa.
*/
{
int i;
char coordenadas[3];
for (i=0; i<grupoNaufragos.cantidad; i++) {
de_fila_y_columna_a_numero_y_letra(grupoNaufragos.naufrago[i].fila,
grupoNaufragos.naufrago[i].columna,
coordenadas);
printf ("N´aufrago %d en coordenadas %s ", i, coordenadas);
if (grupoNaufragos.naufrago[i].encontrado)
printf ("ya ha sido encontrado.n");
else
printf ("sigue perdido.n");
}
}
La funci´on est´a bien, pero podemos mejorarla. F´ıjate en c´omo pasamos su par´ametro: por valor.
¿Por qu´e? Porque no vamos a modificar su valor en el interior de la funci´on. En principio, la
decisi´on de pasarlo por valor est´a bien fundamentada. No obstante, piensa en qu´e ocurre cada
vez que llamamos a la funci´on: como un registro de tipo struct GrupoNaufragos ocupa 64
bytes (haz cuentas y compru´ebalo), cada llamada a la funci´on obliga a copiar 64 bytes en la
pila. El problema se agravar´ıa si en lugar de trabajar con un n´umero m´aximo de 5 n´aufragos lo
hici´eramos con una cantidad mayor. ¿Es realmente necesario ese esfuerzo? La verdad es que no:
podemos limitarnos a copiar 4 bytes si pasamos una referencia al registro. Esta nueva versi´on
de la funci´on efect´ua el paso por referencia:
void muestra_naufragos(struct GrupoNaufragos * grupoNaufragos)
/* Muestra por pantalla las coordenadas de cada n´aufrago e informa de si sigue perdido.
* ´Util para depuraci´on del programa.
*/
{
int i, fila, columna;
char coordenadas[3];
for (i=0; i<grupoNaufragos ->cantidad; i++) {
de_fila_y_columna_a_numero_y_letra(grupoNaufragos ->naufrago[i].fila,
grupoNaufragos ->naufrago[i].columna,
coordenadas);
printf ("N´aufrago %d en coordenadas %s ", i, coordenadas);
if (grupoNaufragos ->naufrago[i].encontrado)
180 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
printf ("ya ha sido encontrado.n");
else
printf ("sigue perdido.n");
}
}
Es posible usar el adjetivo const para dejar claro que pasamos el puntero por eficiencia,
pero no porque vayamos a modificar su contenido:
void muestra_naufragos(const struct GrupoNaufragos * grupoNaufragos)
Hagamos una prueba para ver si todo va bien por el momento:
1 ...
2
3 int main(void)
4 {
5 struct GrupoNaufragos losNaufragos;
6
7 pon_naufragos(&losNaufragos, 5);
8 muestra_naufragos(&losNaufragos);
9
10 return 0;
11 }
Compilemos y ejecutemos el programa. He aqu´ı el resultado:
$ gcc minigalaxis.c -o minigalaxis
$ minigalaxis
N´aufrago 0 en coordenadas 1G sigue perdido.
N´aufrago 1 en coordenadas 0P sigue perdido.
N´aufrago 2 en coordenadas 5P sigue perdido.
N´aufrago 3 en coordenadas 1M sigue perdido.
N´aufrago 4 en coordenadas 6B sigue perdido.
Bien: cada n´aufrago ocupa una posici´on diferente. Ejecut´emoslo de nuevo
$ minigalaxis
N´aufrago 0 en coordenadas 1G sigue perdido.
N´aufrago 1 en coordenadas 0P sigue perdido.
N´aufrago 2 en coordenadas 5P sigue perdido.
N´aufrago 3 en coordenadas 1M sigue perdido.
N´aufrago 4 en coordenadas 6B sigue perdido.
¡Eh! ¡Se han ubicado en las mismas posiciones! ¿Qu´e gracia tiene el juego si en todas las
partidas aparecen los n´aufragos en las mismas casillas? ¿C´omo es posible que ocurra algo as´ı?
¿No se generaba su ubicaci´on al azar? S´ı y no. La funci´on rand genera n´umeros pseudoaleatorios.
Utiliza una f´ormula matem´atica que genera una secuencia de n´umeros de forma tal que no
podemos efectuar una predicci´on del siguiente (a menos que conozcamos la f´ormula, claro est´a).
La secuencia de n´umeros se genera a partir de un n´umero inicial: la semilla. En principio, la
semilla es siempre la misma, as´ı que la secuencia de n´umeros es, tambi´en, siempre la misma.
¿Qu´e hacer, pues, si queremos obtener una diferente? Una posibilidad es solicitar al usuario el
valor de la semilla, que se puede modificar con la funci´on srand, pero no parece lo adecuado
para un juego de ordenador (el usuario podr´ıa hacer trampa introduciendo siempre la misma
semilla). Otra posibilidad es inicializar la semilla con un valor aleatorio. ¿Con un valor aleatorio?
Tenemos un pez que se muerde la cola: ¡resulta que necesito un n´umero aleatorio para generar
n´umeros aleatorios! Mmmmm. Tranquilo, hay una soluci´on: consultar el reloj del ordenador y
usar su valor como semilla. La funci´on time (disponible incluyendo time.h) nos devuelve el
n´umero de segundos transcurridos desde el inicio del d´ıa 1 de enero de 1970 (lo que se conoce
por tiempo de la era Unix) y, naturalmente, es diferente cada vez que lo llamamos para iniciar
una partida. Aqu´ı tienes la soluci´on:
1 ...
2 #include <time.h>
3 ...
4
Introducci´on a la Programaci´on con C 181
3.5 Paso de par´ametros
5 int main(void)
6 {
7 struct GrupoNaufragos losNaufragos;
8
9 srand(time(0));
10
11 pon_naufragos(&losNaufragos, 5);
12 muestra_naufragos(&losNaufragos);
13
14 return 0;
15 }
Efectuemos nuevas pruebas:
$ gcc minigalaxis.c -o minigalaxis
$ minigalaxis
N´aufrago 0 en coordenadas 6K sigue perdido.
N´aufrago 1 en coordenadas 5L sigue perdido.
N´aufrago 2 en coordenadas 6E sigue perdido.
N´aufrago 3 en coordenadas 3I sigue perdido.
N´aufrago 4 en coordenadas 8T sigue perdido.
¡Bravo! Son valores diferentes de los anteriores. Ejecutemos nuevamente el programa:
$ minigalaxis
N´aufrago 0 en coordenadas 2D sigue perdido.
N´aufrago 1 en coordenadas 4H sigue perdido.
N´aufrago 2 en coordenadas 5J sigue perdido.
N´aufrago 3 en coordenadas 4E sigue perdido.
N´aufrago 4 en coordenadas 7G sigue perdido.
¡Perfecto! A otra cosa.
Ya hemos inicializado el tablero y dispuesto los n´aufragos en posiciones al azar. Dise˜nemos
una funci´on para el lanzamiento de sondas. La funci´on (que ser´a un procedimiento) recibir´a un
par de coordenadas, el tablero de juego y el registro que contiene la posici´on de los n´aufragos
y har´a lo siguiente:
modificar´a el tablero de juego sustituyendo los s´ımbolos ’+’ por ’.’ en las direcciones
cardinales desde el punto de lanzamiento de la sonda,
y modificar´a la casilla en la que se lanz´o la sonda indicando el n´umero de n´aufragos
detectados, o marc´andola con una ’X’ si hay un n´aufrago en ella.
1 ...
2 #define NO_SONDEADA ’+’
3 #define RESCATADO ’X’
4 #define SONDEADA ’.’
5 ...
6
7 void lanzar_sonda(int fila, int columna, char tablero[][COLUMNAS],
8 const struct GrupoNaufragos * grupoNaufragos)
9 /* Lanza una sonda en las coordenadas indicadas. Actualiza el tablero con el resultado del
10 * sondeo. Si se detecta un n´aufrago en el punto de lanzamiento de la sonda, lo rescata.
11 */
13 {
14 int detectados = 0, i;
15
16 // Recorrer la vertical
17 for (i=0; i<FILAS; i++) {
18 if (hay_naufrago(i, columna, grupoNaufragos))
19 detectados++;
20 if (tablero[i][columna] == NO_SONDEADA)
21 tablero[i][columna] = SONDEADA;
182 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
22 }
23
24 // Recorrer la horizontal
25 for (i=0; i<COLUMNAS; i++) {
26 if (hay_naufrago(fila, i, grupoNaufragos))
27 detectados++;
28 if (tablero[fila][i] == NO_SONDEADA)
29 tablero[fila][i] = SONDEADA;
30 }
31
32 // Ver si acertamos y hay un n´aufrago en esta misma casilla.
33 if (hay_naufrago(fila, columna, grupoNaufragos)) {
34 tablero[fila][columna] = RESCATADO; // En tal caso, ponemos una X.
35 rescate(fila, columna, grupoNaufragos);
36 }
37 else
38 tablero[fila][columna] = ’0’ + detectados; // Y si no, el n´umero de n´aufragos detectados.
39 }
Esta funci´on se ayuda con otras dos: hay_naufrago y rescate. La primera nos indica si hay
un n´aufrago en una casilla determinada:
1 int hay_naufrago(int fila, int columna, const struct GrupoNaufragos * grupoNaufragos)
2 /* Averigua si hay un n´aufrago perdido en las coordenadas (fila, columna).
3 * Si lo hay devuelve 1; si no lo hay, devuelve 0.
4 */
6 {
7 int i;
8
9 for (i=0; i<grupoNaufragos->cantidad; i++)
10 if (fila == grupoNaufragos->naufrago[i].fila &&
11 columna == grupoNaufragos->naufrago[i].columna)
12 return 1;
13 return 0;
14 }
Y la segunda lo marca como rescatado:
1 void rescate(int fila, int columna, struct GrupoNaufragos * grupoNaufragos)
2 /* Rescata al n´aufrago que hay en las coordenadas indicadas. */
3 {
4 int i;
5
6 for (i=0; i<grupoNaufragos->cantidad; i++)
7 if (fila == grupoNaufragos->naufrago[i].fila &&
8 columna == grupoNaufragos->naufrago[i].columna)
9 grupoNaufragos->naufrago[i].encontrado = 1;
10 }
Ya podemos ofrecer una versi´on m´as completa del programa principal:
1 int main(void)
2 {
3 char espacio[FILAS][COLUMNAS];
4 struct GrupoNaufragos losNaufragos;
5 char coordenadas[TALLACAD+1];
6 int fila, columna;
7
8 srand(time(0));
9
10 pon_naufragos(&losNaufragos, 5);
11 inicializa_tablero(espacio);
12 muestra_tablero(espacio);
13
14 while ( ??? ) {
Introducci´on a la Programaci´on con C 183
3.5 Paso de par´ametros
15 printf ("Coordenadas: "); scanf ("%s", coordenadas);
16 while (!de_numero_y_letra_a_fila_y_columna(coordenadas, &fila, &columna)) {
17 printf ("Coordenadas no v´alidas. Int´entelo de nuevo.nCoordenadas: ");
18 scanf ("%s", coordenadas);
19 }
20 lanzar_sonda(fila, columna, espacio, &losNaufragos);
21 muestra_tablero(espacio);
22 }
23
24 return 0;
25 }
¿Cu´ando debe finalizar el bucle while exterior? Bien cuando hayamos rescatado a todos los
n´aufragos, bien cuando nos hayamos quedado sin sondas. En el primer caso habremos vencido
y en el segundo habremos perdido:
1 ...
2 #define SONDAS 20
3 ...
4
5 int perdidos(const struct GrupoNaufragos * grupoNaufragos)
6 /* Cuenta el n´umero de n´aufragos que siguen perdidos. */
7 {
8 int contador = 0, i;
9
10 for (i=0; i<grupoNaufragos->cantidad; i++)
11 if (!grupoNaufragos->naufrago[i].encontrado)
12 contador++;
13 return contador;
14 }
15
16 ...
17
18 int main(void)
19 {
20 char espacio[FILAS][COLUMNAS];
21 struct GrupoNaufragos losNaufragos;
22 int sondas_disponibles = SONDAS;
23 char coordenadas[TALLACAD+1];
24 int fila, columna;
25
26 srand(time(0));
27
28 pon_naufragos(&losNaufragos, 5);
29 inicializa_tablero(espacio);
30 muestra_tablero(espacio);
31
32 while (sondas_disponibles > 0 && perdidos(&losNaufragos) > 0) {
33 printf ("Hay %d n´aufragosn", perdidos(&losNaufragos));
34 printf ("Dispones de %d sondasn", sondas_disponibles);
35 printf ("Coordenadas: "); scanf ("%s", coordenadas);
36 while (!de_numero_y_letra_a_fila_y_columna(coordenadas, &fila, &columna)) {
37 printf ("Coordenadas no v´alidas. Int´entelo de nuevo.nCoordenadas: ");
38 scanf ("%s", coordenadas);
39 }
40 lanzar_sonda(fila, columna, espacio, &losNaufragos);
41 muestra_tablero(espacio);
42 sondas_disponibles--;
43 }
44
45 if (perdidos(&losNaufragos) == 0)
46 printf ("Has ganado. Puntuaci´on: %d puntos.n", SONDAS - sondas_disponibles);
47 else
48 printf ("Has perdido. Por tu culpa han muerto %d n´aufragosn",
184 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
49 perdidos(&losNaufragos));
50
51 return 0;
52 }
Hemos definido una nueva funci´on, perdidos, que calcula el n´umero de n´aufragos que per-
manecen perdidos.
Y ya est´a. Te mostramos finalmente el listado completo del programa:
minigalaxis.c minigalaxis.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <ctype.h>
5 #include <time.h>
6
7 #define FILAS 9
8 #define COLUMNAS 20
9 #define TALLACAD 80
10 #define MAX_NAUFRAGOS 5
11 #define SONDAS 20
12
13 #define NO_SONDEADA ’+’
14 #define RESCATADO ’X’
15 #define SONDEADA ’.’
16
17 /**********************************************************
18 * Conversi´on entre los dos modos de expresar coordenadas
19 **********************************************************/
21
22 void de_fila_y_columna_a_numero_y_letra(int fila, int columna, char coordenadas[])
23 /* Convierte una fila y columna descritas num´ericamente en una fila y columna descritas
24 * como una cadena con un d´ıgito y una letra.
25 */
27 {
28 coordenadas[0] = ’0’ + fila;
29 coordenadas[1] = ’A’ + columna;
30 coordenadas[2] = ’0’;
31 }
32
33 int de_numero_y_letra_a_fila_y_columna(char coordenadas[], int * fila, int * columna)
34 /* Convierte una fila y columna con un d´ıgito y una letra (min´uscula o may´uscula) en
35 * cualquier orden a una fila y columna descritas num´ericamente.
36 */
38 {
39 printf (">>> %sn", coordenadas);
40 if (strlen(coordenadas) != 2)
41 return 0;
42 if (coordenadas[0] >= ’0’ && coordenadas[0] <= ’8’ && isalpha(coordenadas[1])) {
43 *fila = coordenadas[0] - ’0’;
44 *columna = toupper(coordenadas[1]) - ’A’;
45 return 1;
46 }
47 if (coordenadas[1] >= ’0’ && coordenadas[1] <= ’8’ && isalpha(coordenadas[0])) {
48 *columna = toupper(coordenadas[0]) - ’A’;
49 *fila = coordenadas[1] - ’0’;
50 return 1;
51 }
52 return 0;
53 }
54
55 /****************************************
56 * N´aufragos
57 ****************************************/
Introducci´on a la Programaci´on con C 185
3.5 Paso de par´ametros
59
60 struct Naufrago {
61 int fila, columna; // Coordenadas
62 int encontrado; //
?
Ha sido encontrado ya?
63 };
64
65 struct GrupoNaufragos {
66 struct Naufrago naufrago[MAX_NAUFRAGOS];
67 int cantidad;
68 };
69
70 void pon_naufragos(struct GrupoNaufragos * grupoNaufragos, int cantidad)
71 /* Situa aleatoriamente cantidad n´aufragos en la estructura grupoNaufragos. */
72 {
73 int fila, columna, ya_hay_uno_ahi, i;
74
75 grupoNaufragos->cantidad = 0;
76 while (grupoNaufragos->cantidad != cantidad) {
77 fila = rand() % FILAS;
78 columna = rand() % COLUMNAS;
79 ya_hay_uno_ahi = 0;
80 for (i=0; i<grupoNaufragos->cantidad; i++)
81 if (fila == grupoNaufragos->naufrago[i].fila &&
82 columna == grupoNaufragos->naufrago[i].columna) {
83 ya_hay_uno_ahi = 1;
84 break;
85 }
86 if (!ya_hay_uno_ahi) {
87 grupoNaufragos->naufrago[grupoNaufragos->cantidad].fila = fila;
88 grupoNaufragos->naufrago[grupoNaufragos->cantidad].columna = columna;
89 grupoNaufragos->naufrago[grupoNaufragos->cantidad].encontrado = 0;
90 grupoNaufragos->cantidad++;
91 }
92 }
93 }
94
95 int hay_naufrago(int fila, int columna, const struct GrupoNaufragos * grupoNaufragos)
96 /* Averigua si hay un n´aufrago perdido en las coordenadas (fila, columna).
97 * Si lo hay devuelve 1; si no lo hay, devuelve 0.
98 */
100 {
101 int i;
102
103 for (i=0; i<grupoNaufragos->cantidad; i++)
104 if (fila == grupoNaufragos->naufrago[i].fila &&
105 columna == grupoNaufragos->naufrago[i].columna)
106 return 1;
107 return 0;
108 }
109
110
111 void rescate(int fila, int columna, struct GrupoNaufragos * grupoNaufragos)
112 /* Rescata al n´aufrago que hay en las coordenadas indicadas. */
113 {
114 int i;
115
116 for (i=0; i<grupoNaufragos->cantidad; i++)
117 if (fila == grupoNaufragos->naufrago[i].fila &&
118 columna == grupoNaufragos->naufrago[i].columna)
119 grupoNaufragos->naufrago[i].encontrado = 1;
120 }
121
122 int perdidos(const struct GrupoNaufragos * grupoNaufragos)
186 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
123 /* Cuenta el n´umero de n´aufragos que siguen perdidos. */
124 {
125 int contador = 0, i;
126
127 for (i=0; i<grupoNaufragos->cantidad; i++)
128 if (!grupoNaufragos->naufrago[i].encontrado)
129 contador++;
130 return contador;
131 }
132
133 void muestra_naufragos(const struct GrupoNaufragos * grupoNaufragos)
134 /* Muestra por pantalla las coordenadas de cada naufrago e informa de si sigue perdido.
135 * ´Util para depuraci´on del programa.
136 */
138 {
139 int i;
140 char coordenadas[3];
141
142 for (i=0; i<grupoNaufragos->cantidad; i++) {
143 de_fila_y_columna_a_numero_y_letra(grupoNaufragos->naufrago[i].fila,
144 grupoNaufragos->naufrago[i].columna,
145 coordenadas);
146 printf ("Naufrago %d en coordenadas %s ", i, coordenadas);
147 if (grupoNaufragos->naufrago[i].encontrado)
148 printf ("ya ha sido encontrado.n");
149 else
150 printf ("sigue perdido.n");
151 }
152 }
153
154 /****************************************
155 * Tablero
156 ****************************************/
158
159 void inicializa_tablero(char tablero[][COLUMNAS])
160 /* Inicializa el tablero de juego marcando todas las casillas como no sondeadas. */
161 {
162 int i, j;
163
164 for (i=0; i<FILAS; i++)
165 for (j=0; j<COLUMNAS; j++)
166 tablero[i][j] = NO_SONDEADA;
167 }
168
169 void muestra_tablero(char tablero[][COLUMNAS])
170 /* Muestra en pantalla el tablero de juego. */
171 {
172 int i, j;
173
174 // Etiquetar con una letra cada columna.
175 printf (" ");
176 for (j=0; j<COLUMNAS; j++) printf ("%c", ’A’+j);
177 printf ("n");
178
179 for (i=0; i<FILAS; i++) {
180 printf ("%d ", i); // Etiqueta de cada fila.
181 for (j=0; j<COLUMNAS; j++)
182 printf ("%c", tablero[i][j]);
183 printf ("n");
184 }
185 }
186
187 /****************************************
Introducci´on a la Programaci´on con C 187
3.5 Paso de par´ametros
188 * Sonda
189 ****************************************/
191
192 void lanzar_sonda(int fila, int columna, char tablero[][COLUMNAS],
193 struct GrupoNaufragos * grupoNaufragos)
194 /* Lanza una sonda en las coordenadas indicadas. Actualiza el tablero con el resultado del
195 * sondeo. Si se detecta un n´aufrago en el punto de lanzamiento de la sonda, lo rescata.
196 */
198 {
199 int detectados = 0, i;
200
201 // Recorrer la vertical
202 for (i=0; i<FILAS; i++) {
203 if (hay_naufrago(i, columna, grupoNaufragos))
204 detectados++;
205 if (tablero[i][columna] == NO_SONDEADA)
206 tablero[i][columna] = SONDEADA;
207 }
208
209 // Recorrer la horizontal
210 for (i=0; i<COLUMNAS; i++) {
211 if (hay_naufrago(fila, i, grupoNaufragos))
212 detectados++;
213 if (tablero[fila][i] == NO_SONDEADA)
214 tablero[fila][i] = SONDEADA;
215 }
216
217 // Ver si acertamos y hay una n´aufrago en esta misma casilla.
218 if (hay_naufrago(fila, columna, grupoNaufragos)) {
219 tablero[fila][columna] = RESCATADO; // En tal caso, ponemos una X.
220 rescate(fila, columna, grupoNaufragos);
221 }
222 else
223 tablero[fila][columna] = ’0’ + detectados; // Y si no, el n´umero de n´aufragos detectados.
224 }
225
226 int main(void)
227 {
228 char espacio[FILAS][COLUMNAS];
229 struct GrupoNaufragos losNaufragos;
230 int sondas_disponibles = SONDAS;
231 char coordenadas[TALLACAD+1];
232 int fila, columna;
233
234 srand(time(0));
235
236 pon_naufragos(&losNaufragos, 5);
237 inicializa_tablero(espacio);
238 muestra_tablero(espacio);
239
240 while (sondas_disponibles > 0 && perdidos(&losNaufragos) > 0) {
241 printf ("Hay %d n´aufragosn", perdidos(&losNaufragos));
242 printf ("Dispones de %d sondasn", sondas_disponibles);
243 printf ("Coordenadas: "); scanf ("%s", coordenadas);
244 while (!de_numero_y_letra_a_fila_y_columna(coordenadas, &fila, &columna)) {
245 printf ("Coordenadas no v´alidas. Int´entelo de nuevo.nCoordenadas: ");
246 scanf ("%s", coordenadas);
247 }
248 lanzar_sonda(fila, columna, espacio, &losNaufragos);
249 muestra_tablero(espacio);
250 sondas_disponibles--;
251 }
252
188 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
253 if (perdidos(&losNaufragos) == 0)
254 printf ("Has ganado. Puntuaci´on: %d puntos.n", SONDAS - sondas_disponibles);
255 else
256 printf ("Has perdido. Por tu culpa han muerto %d n´aufragosn",
257 perdidos(&losNaufragos));
258
259 return 0;
260 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 202 Reescribe el programa para que no se use una variable de tipo struct GrupoNaufragos
como almac´en del grupo de n´aufragos, sino una matriz paralela a la matriz espacio.
Cada n´aufrago se representar´a con un ’*’ mientras permanezca perdido, y con una ’X’
cuando haya sido descubierto.
· 203 Siempre que usamos rand en miniGalaxis calculamos un par de n´umeros aleatorios.
Hemos definido un nuevo tipo y una funci´on:
1 struct Casilla {
2 int fila, columna;
3 };
4
5 struct Casilla casilla_al_azar(void)
6 {
7 struct Casilla casilla;
8
9 casilla.fila = rand() % FILAS;
10 casilla.columna = rand() % COLUMNAS;
11 return casilla;
12 }
Y proponemos usarlos as´ı:
1 void pon_naufragos(struct GrupoNaufragos * grupoNaufragos, int cantidad)
2 /* Situa aleatoriamente cantidad n´aufragos en la estructura grupoNaufragos. */
3 {
4 int fila, columna, ya_hay_uno_ahi, i;
5 struct Casilla una_casilla;
6
7 grupoNaufragos->cantidad = 0;
8 while (grupoNaufragos->cantidad != cantidad) {
9 una_casilla = casilla_al_azar();
10 ya_hay_uno_ahi = 0;
11 for (i=0; i<grupoNaufragos->cantidad; i++)
12 if (una_casilla.fila == grupoNaufragos->naufrago[i].fila &&
13 una_casilla.columna == grupoNaufragos->naufrago[i].columna) {
14 ya_hay_uno_ahi = 1;
15 break;
16 }
17 if (!ya_hay_uno_ahi) {
18 grupoNaufragos->naufrago[grupoNaufragos->cantidad].fila = una_casilla.fila ;
19 grupoNaufragos->naufrago[grupoNaufragos->cantidad].columna = una_casilla.columna ;
20 grupoNaufragos->naufrago[grupoNaufragos->cantidad].encontrado = 0;
21 grupoNaufragos->cantidad++;
22 }
23 }
24 }
¿Es correcto el programa con estos cambios?
· 204 Como siempre que usamos rand calculamos un par de n´umeros aleatorios, hemos mo-
dificado el programa de este modo:
1 struct Naufrago naufrago_al_azar(void)
2 {
Introducci´on a la Programaci´on con C 189
3.6 Recursi´on
3 struct Naufrago naufrago;
4
5 naufrago.fila = rand() % FILAS;
6 naufrago.columna = rand() % COLUMNAS;
7 naufrago.encontrado = 0;
8 return naufrago;
9 }
10
11 void pon_naufragos(struct GrupoNaufragos * grupoNaufragos, int cantidad)
12 /* Situa aleatoriamente cantidad n´aufragos en la estructura grupoNaufragos. */
13 {
14 int fila, columna, ya_hay_uno_ahi, i;
15 struct Naufrago un_naufrago;
16
17 grupoNaufragos->cantidad = 0;
18 while (grupoNaufragos->cantidad != cantidad) {
19 un_naufrago = naufrago_al_azar();
20 ya_hay_uno_ahi = 0;
21 for (i=0; i<grupoNaufragos->cantidad; i++)
22 if (un_naufrago.fila == grupoNaufragos->naufrago[i].fila &&
23 un_naufrago.columna == grupoNaufragos->naufrago[i].columna) {
24 ya_hay_uno_ahi = 1;
25 break;
26 }
27 if (!ya_hay_uno_ahi) {
28 grupoNaufragos->naufrago[grupoNaufragos->cantidad] = un_naufrago ;
29 grupoNaufragos->cantidad++;
30 }
31 }
32 }
¿Es correcto el programa con estos cambios?
· 205 Modifica el juego para que el usuario pueda escoger el nivel de dificultad. El usuario
escoger´a el n´umero de n´aufragos perdidos (con un m´aximo de 20) y el n´umero de sondas
disponibles.
· 206 Hemos construido una versi´on simplificada de Galaxis. El juego original s´olo se dife-
rencia de ´este en las direcciones exploradas por la sonda: as´ı como las sondas de miniGalaxis
exploran 4 direcciones, las de Galaxis exploran 8. Te mostramos el resultado de lanzar nuestra
primera sonda en las coordenadas 4J de un tablero de juego Galaxis:
ABCDEFGHIJKLMNOPQRST
0 +++++.+++.+++.++++++
1 ++++++.++.++.+++++++
2 +++++++.+.+.++++++++
3 ++++++++...+++++++++
4 .........1..........
5 ++++++++...+++++++++
6 +++++++.+.+.++++++++
7 ++++++.++.++.+++++++
8 +++++.+++.+++.++++++
Implementa el juego Galaxis.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.6. Recursi´on
Es posible definir funciones recursivas en C. La funci´on factorial de este programa, por ejemplo,
define un c´alculo recursivo del factorial:
factorial recursivo.c factorial recursivo.c
1 #include <stdio.h>
2
190 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
3 int factorial (int n)
4 {
5 if (n<=1)
6 return 1;
7 else
8 return n * factorial (n-1);
9 }
10
11 int main(void)
12 {
13 int valor;
14
15 printf ("Dame un n´umero entero positivo: ");
16 scanf ("%d", &valor);
17 printf ("El factorial de %d vale: %dn", valor, factorial(valor));
18
19 return 0;
20 }
Nada nuevo. Ya conoces el concepto de recursi´on de Python. En C es lo mismo. Tiene inter´es,
eso s´ı, que estudiemos brevemente el aspecto de la memoria en un instante dado. Por ejemplo,
cuando llamamos a factorial(5), que ha llamado a factorial(4), que a su vez ha llamado a
factorial(3), la pila presentar´a esta configuraci´on:
main 5valor
llamada desde l´ınea 17
factorial 5n
llamada desde l´ınea 8
factorial 4n
llamada desde l´ınea 8
factorial 3n
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 207 Dise˜na una funci´on que calcule recursivamente xn
. La variable x ser´a de tipo float y
n de tipo int.
· 208 Dise˜na una funci´on recursiva que calcule el n-´esimo n´umero de Fibonacci.
· 209 Dise˜na una funci´on recursiva para calcular el n´umero combinatorio n sobre m sabiendo
que
n
n
= 1,
n
0
= 1,
n
m
=
n − 1
m
+
n − 1
m − 1
.
· 210 Dise˜na un procedimiento recursivo llamado muestra_bin que reciba un n´umero en-
tero positivo y muestre por pantalla su codificaci´on en binario. Por ejemplo, si llamamos a
muestra_bin(5), por pantalla aparecer´a el texto ((101)).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.6.1. Un m´etodo recursivo de ordenaci´on: mergesort
Vamos a estudiar ahora un m´etodo recursivo de ordenaci´on de vectores: mergesort (que se
podr´ıa traducir por ordenaci´on por fusi´on o mezcla). Estudiemos primero la aproximaci´on que
Introducci´on a la Programaci´on con C 191
3.6 Recursi´on
sigue considerando un procedimiento equivalente para ordenar las 12 cartas de un palo de la
baraja de cartas. La ordenaci´on por fusi´on de un palo de la baraja consiste en lo siguiente:
Dividir el paquete de cartas en dos grupos de 6 cartas;
ordenar por fusi´on el primer grupo de 6 cartas;
ordenar por fusi´on el segundo grupo de 6 cartas;
fundir los dos grupos, que ya est´an ordenados, tomando siempre la carta con n´umero
menor de cualquiera de los dos grupos (que siempre ser´a la primera de uno de los dos
grupos).
Ya ves d´onde aparece la recursi´on, ¿no? Para ordenar 12 cartas por fusi´on hemos de ordenar
dos grupos de 6 cartas por fusi´on. Y para ordenar cada grupo de 6 cartas por fusi´on tendremos
que ordenar dos grupos de 3 cartas por fusi´on. Y para ordenar 3 grupos de cartas por fusi´on. . .
¿Cu´ando finaliza la recursi´on? Cuando nos enfrentemos a casos triviales. Ordenar un grupo de
1 sola carta es trivial: ¡siempre est´a ordenado!
Desarrollemos un ejemplo de ordenaci´on de un vector con 16 elementos:
11
0
21
1
3
2
1
3
98
4
0
5
12
6
82
7
29
8
30
9
11
10
18
11
43
12
4
13
75
14
37
15
1. Empezamos separando el vector en dos ((subvectores)) de 8 elementos:
11
0
21
1
3
2
1
3
98
4
0
5
12
6
82
7
29
8
30
9
11
10
18
11
43
12
4
13
75
14
37
15
2. ordenamos por fusi´on el primer vector, con lo que obtenemos:
0
0
1
1
3
2
11
3
12
4
21
5
82
6
98
7
3. y ordenamos por fusi´on el segundo vector, con lo que obtenemos:
4
0
11
1
18
2
29
3
30
4
37
5
43
6
75
7
4. y ahora ((fundimos)) ambos vectores ordenados, obteniendo as´ı un ´unico vector ordenado:
0
0
1
1
3
2
4
3
11
4
11
5
12
6
18
7
21
8
29
9
30
10
37
11
43
12
75
13
82
14
98
15
La idea b´asica de la fusi´on es sencilla: se recorren ambos vectores de izquierda a derecha,
seleccionando en cada momento el menor elemento posible. Los detalles del proceso de
fusi´on son un tanto escabrosos, as´ı que lo estudiaremos con calma un poco m´as adelante.
Podemos representar el proceso realizado con esta imagen gr´afica:
11
0
21
1
3
2
1
3
98
4
0
5
12
6
82
7
29
8
30
9
11
10
18
11
43
12
4
13
75
14
37
15
11
0
21
1
3
2
1
3
98
4
0
5
12
6
82
7
29
8
30
9
11
10
18
11
43
12
4
13
75
14
37
15
0
0
1
1
3
2
11
3
12
4
21
5
82
6
98
7
4
8
11
9
18
10
29
11
30
12
37
13
43
14
75
15
0
0
1
1
3
2
4
3
11
4
11
5
12
6
18
7
21
8
29
9
30
10
37
11
43
12
75
13
82
14
98
15
dividir el problema (de talla 16)
en dos problemas (de talla 8),
resolver independientemente cada problema
y combinar ambas soluciones.
192 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
Est´a claro que hemos hecho ((trampa)): las l´ıneas de trazo discontinuo esconden un proceso
complejo, pues la ordenaci´on de cada uno de los vectores de 8 elementos supone la ordenaci´on
(recursiva) de dos vectores de 4 elementos, que a su vez. . . ¿Cu´ando acaba el proceso recursivo?
Cuando llegamos a un caso trivial: la ordenaci´on de un vector que s´olo tenga 1 elemento.
He aqu´ı el proceso completo:
11
0
21
1
3
2
1
3
98
4
0
5
12
6
82
7
29
8
30
9
11
10
18
11
43
12
4
13
75
14
37
15
11
0
21
1
3
2
1
3
98
4
0
5
12
6
82
7
29
8
30
9
11
10
18
11
43
12
4
13
75
14
37
15
11
0
21
1
3
2
1
3
98
4
0
5
12
6
82
7
29
8
30
9
11
10
18
11
43
12
4
13
75
14
37
15
11
0
21
1
3
2
1
3
98
4
0
5
12
6
82
7
29
8
30
9
11
10
18
11
43
12
4
13
75
14
37
15
11
0
21
1
3
2
1
3
98
4
0
5
12
6
82
7
29
8
30
9
11
10
18
11
43
12
4
13
75
14
37
15
11
0
21
1
1
2
3
3
0
4
98
5
12
6
82
7
29
8
30
9
11
10
18
11
4
12
43
13
37
14
75
15
1
0
3
1
11
2
21
3
0
4
12
5
82
6
98
7
11
8
18
9
29
10
30
11
4
12
37
13
43
14
75
15
0
0
1
1
3
2
11
3
12
4
21
5
82
6
98
7
4
8
11
9
18
10
29
11
30
12
37
13
43
14
75
15
0
0
1
1
3
2
4
3
11
4
11
5
12
6
18
7
21
8
29
9
30
10
37
11
43
12
75
13
82
14
98
15
DivisionesFusiones
Nos queda por estudiar con detalle el proceso de fusi´on. Desarrollemos primero una funci´on
que recoja la idea b´asica de la ordenaci´on por fusi´on: se llamar´a mergesort y recibir´a un vector
v y, en principio, la talla del vector que deseamos ordenar. Esta funci´on utilizar´a una funci´on
auxiliar merge encargada de efectuar la fusi´on de vectores ya ordenados. Aqu´ı tienes un borrador
incompleto:
1 void mergesort(int v[], int talla)
2 {
3 if (talla == 1)
4 return;
5 else {
6 mergesort ( la primera mitad de v );
7 mergesort ( la segunda mitad de v );
8 merge(la primera mitad de v, la segunda mitad de v);
9 }
10 }
Dejemos para m´as adelante el desarrollo de merge. De momento, el principal problema es
c´omo expresar lo de ((la primera mitad de v)) y ((la segunda mitad de v)). F´ıjate: en el fondo,
se trata de se˜nalar una serie de elementos consecutivos del vector v. Cuando orden´abamos el
vector del ejemplo ten´ıamos:
11
0
21
1
3
2
1
3
98
4
0
5
12
6
82
7
29
8
30
9
11
10
18
11
43
12
4
13
75
14
37
15
El primer ((subvector)) es la serie de valores entre el primer par de flechas, y el segundo
((subvector)) es la serie entre el segundo par de flechas. Modifiquemos, pues, mergesort para que
Introducci´on a la Programaci´on con C 193
3.6 Recursi´on
trabaje con ((subvectores)), es decir, con un vector e ´ındices que se˜nalan d´onde empieza y d´onde
acaba cada serie de valores.
1 void mergesort(int v[], int inicio , int final )
2 {
3 if (final - inicio == 0)
4 return;
5 else {
6 mergesort ( v, inicio , (inicio+final) / 2);
7 mergesort ( v, (inicio+final) / 2 + 1, final );
8 merge(la primera mitad de v, la segunda mitad de v);
9 }
10 }
Perfecto. Acabamos de expresar la idea de dividir un vector en dos sin necesidad de utilizar
nuevos vectores.
Nos queda por detallar la funci´on merge. Dicha funci´on recibe dos ((subvectores)) contiguos
ya ordenados y los funde, haciendo que la zona de memoria que ambos ocupan pase a estar
completamente ordenada. Este gr´afico muestra c´omo se fundir´ıan, paso a paso, dos vectores, a
y b para formar un nuevo vector c. Necesitamos tres ´ındices, i, j y k, uno para cada vector:
1
0
3
1
11
2
21
3
0
0
12
1
82
2
98
3 0 1 2 3 4 5 6 7
i j k
Inicialmente, los tres ´ındices valen 0. Ahora comparamos a[i] con b[j], seleccionamos el menor
y almacenamos el valor en c[k]. Es necesario incrementar i si escogimos un elemento de a y j
si lo escogimos de b. En cualquier caso, hemos de incrementar tambi´en la variable k:
1
0
3
1
11
2
21
3
0
0
12
1
82
2
98
3
0
0 1 2 3 4 5 6 7
i j k
El proceso se repite hasta que alguno de los dos primeros ´ındices, i o j, se ((sale)) del vector
correspondiente, tal y como ilustra esta secuencia de im´agenes:
1
0
3
1
11
2
21
3
0
0
12
1
82
2
98
3
0
0
1
1 2 3 4 5 6 7
i j k
1
0
3
1
11
2
21
3
0
0
12
1
82
2
98
3
0
0
1
1
3
2 3 4 5 6 7
i j k
1
0
3
1
11
2
21
3
0
0
12
1
82
2
98
3
0
0
1
1
3
2
11
3 4 5 6 7
i j k
1
0
3
1
11
2
21
3
0
0
12
1
82
2
98
3
0
0
1
1
3
2
11
3
12
4 5 6 7
i j k
1
0
3
1
11
2
21
3
0
0
12
1
82
2
98
3
0
0
1
1
3
2
11
3
12
4
21
5 6 7
i j k
Ahora, basta con copiar los ´ultimos elementos del otro vector al final de c:
194 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
1
0
3
1
11
2
21
3
0
0
12
1
82
2
98
3
0
0
1
1
3
2
11
3
12
4
21
5
82
6 7
i j k
1
0
3
1
11
2
21
3
0
0
12
1
82
2
98
3
0
0
1
1
3
2
11
3
12
4
21
5
82
6
98
7
i j k
Un ´ultimo paso del proceso de fusi´on deber´ıa copiar los elementos de c en a y b, que en realidad
son fragmentos contiguos de un mismo vector.
Vamos a por los detalles de implementaci´on. No trabajamos con dos vectores independientes,
sino con un s´olo vector en el que se marcan ((subvectores)) con pares de ´ındices.
1 void merge(int v[], int inicio1, int final1, int inicio2, int final2)
2 {
3 int i, j, k;
4 int c[final2-inicio1+1]; // Vector de talla determinada en tiempo de ejecuci´on.
5
6 i = inicio1;
7 j = inicio2;
8 k = 0;
9
10 while (i<=final1 && j<=final2)
11 if (v[i] < v[j])
12 c[k++] = v[i++];
13 else
14 c[k++] = v[j++];
15
16 while (i<=final1)
17 c[k++] = v[i++];
18
19 while (j<=final2)
20 c[k++] = v[j++];
21
22 for (k=0; k<final2-inicio1+1; k++)
23 v[inicio1+k] = c[k];
24 }
El ´ultimo paso del procedimiento se encarga de copiar los elementos de c en el vector original.
Ya est´a. Bueno, a´un podemos efectuar una mejora para reducir el n´umero de par´ametros:
f´ıjate en que inicio2 siempre es igual a final1+1. Podemos prescindir de uno de los dos par´ametros:
1 void merge(int v[], int inicio1, int final1, int final2)
2 {
3 int i, j, k;
4 int c[final2-inicio1+1];
5
6 i = inicio1;
7 j = final1+1;
8 k = 0;
9
10 while (i<=final1 && j<=final2)
11 if (v[i] < v[j])
12 c[k++] = v[i++];
13 else
14 c[k++] = v[j++];
15
16 while (i<=final1)
17 c[k++] = v[i++];
18
19 while (j<=final2)
20 c[k++] = v[j++];
Introducci´on a la Programaci´on con C 195
3.6 Recursi´on
21
22 for (k=0; k<final2-inicio1+1; k++)
23 v[inicio1+k] = c[k];
24 }
Veamos c´omo quedar´ıa un programa completo que use mergesort:
ordena.c ordena.c
1 #include <stdio.h>
2
3 #define TALLA 100
4
5 void merge(int v[], int inicio1, int final1, int final2)
6 {
7 int i, j, k;
8 int c[final2-inicio1+1];
9
10 i = inicio1;
11 j = final1+1;
12 k = 0;
13
14 while (i<=final1 && j<=final2)
15 if (v[i] < v[j])
16 c[k++] = v[i++];
17 else
18 c[k++] = v[j++];
19
20 while (i<=final1)
21 c[k++] = v[i++];
22
23 while (j<=final2)
24 c[k++] = v[j++];
25
26 for (k=0; k<final2-inicio1+1; k++)
27 v[inicio1+k] = c[k];
28 }
29
30 void mergesort(int v[], int inicio, int final)
31 {
32 if (final - inicio == 0)
33 return;
34 else {
35 mergesort ( v, inicio, (inicio+final) / 2 );
36 mergesort ( v, (inicio+final) / 2 + 1, final );
37 merge( v, inicio, (inicio+final) / 2, final );
38 }
39 }
40
41 int main(void)
42 {
43 int mivector[TALLA];
44 int i, talla;
45
46 talla = 0;
47 for (i=0; i<TALLA; i++) {
48 printf ("Introduce elemento %d (negativo para acabar): ", i);
49 scanf ("%d", &mivector[i]);
50 if (mivector[i] < 0)
51 break;
52 talla++;
53 }
54
55 mergesort(mivector, 0, talla-1);
56
196 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
57 printf ("Vector ordenado:n");
58 for (i=0; i<talla; i++)
59 printf ("%d ", mivector[i]);
60 printf ("n");
61 return 0;
62 }
He aqu´ı una ejecuci´on del programa:
Introduce elemento 0 (negativo para acabar): 3
Introduce elemento 1 (negativo para acabar): 53
Introduce elemento 2 (negativo para acabar): 32
Introduce elemento 3 (negativo para acabar): 34
Introduce elemento 4 (negativo para acabar): 64
Introduce elemento 5 (negativo para acabar): 3
Introduce elemento 6 (negativo para acabar): 4
Introduce elemento 7 (negativo para acabar): 6
Introduce elemento 8 (negativo para acabar): 7
Introduce elemento 9 (negativo para acabar): -1
Vector ordenado:
3 3 4 6 7 32 34 53 64
Mergesort y el estilo C
Los programadores C tienden a escribir los programas de una forma muy compacta. Estudia
esta nueva versi´on de la funci´on merge:
1 void merge(int v[], int inicio1, int final1, int final2)
2 {
3 int i, j, k;
4 int c[final2-inicio1+1];
5
6 for (i=inicio1, j=final1+1, k=0; i<=final1 && j<=final2; )
7 c[k++] = (v[i] < v[j]) ? v[i++] : v[j++];
8 while (i<=final1) c[k++] = v[i++];
9 while (j<=final2) c[k++] = v[j++];
10 for (k=0; k<final2-inicio1+1; k++) v[inicio1+k] = c[k];
11 }
Observa que los bucles for aceptan m´as de una inicializaci´on (separ´andolas por comas)
y permiten que alguno de sus elementos est´e en blanco (en el primer for la acci´on de
incremento del´ındice est´a en blanco). No te sugerimos que hagas t´u lo mismo: te prevenimos
para que est´es preparado cuando te enfrentes a la lectura de programas C escritos por otros.
Tambi´en vale la pena apreciar el uso del operador ternario para evitar una estructura
condicional if-else que en sus dos bloques asigna un valor a la misma celda del vector. Es
una pr´actica frecuente y da lugar, una vez acostumbrado, a programas bastante legibles.
3.6.2. Recursi´on indirecta y declaraci´on anticipada
C debe conocer la cabecera de una funci´on antes de que sea llamada, es decir, debe conocer
el tipo de retorno y el n´umero y tipo de sus par´ametros. Normalmente ello no plantea ning´un
problema: basta con definir la funci´on antes de su uso, pero no siempre es posible. Imagina que
una funci´on f necesita llamar a una funci´on g y que g, a su vez, necesita llamar a f (recursi´on
indirecta). ¿Cu´al ponemos delante? La soluci´on es f´acil: da igual, la que quieras, pero debes
hacer una declaraci´on anticipada de la funci´on que defines en segundo lugar. La declaraci´on
anticipada no incluye el cuerpo de la funci´on: consiste en la declaraci´on del tipo de retorno,
identificador de funci´on y lista de par´ametros con su tipo, es decir, es un prototipo o perfil de
la funci´on en cuesti´on.
Estudia este ejemplo6
:
6El ejemplo es meramente ilustrativo: hay formas mucho m´as eficientes de saber si un n´umero es par o impar.
Introducci´on a la Programaci´on con C 197
3.7 Macros
1 int impar(int a);
2
3 int par(int a)
4 {
5 if (a==0)
6 return 1;
7 else
8 return (impar(a-1));
9 }
10
11 int impar(int a)
12 {
13 if (a==0)
14 return 0;
15 else
16 return (par(a-1));
17 }
La primera l´ınea es una declaraci´on anticipada de la funci´on impar, pues se usa antes de haber
sido definida. Con la declaraci´on anticipada hemos ((adelantado)) la informaci´on acerca de qu´e
tipo de valores aceptar´a y devolver´a la funci´on.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 211 Dibuja el estado de la pila cuando se llega al caso base en la llamada recursiva impar(7).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
La declaraci´on anticipada resulta necesaria para programas con recursi´on indirecta, pero
tambi´en la encontrar´as (o usar´as) en programas sin recursi´on. A veces conviene definir funciones
en un orden que facilite la lectura del programa, y es f´acil que se defina una funci´on despu´es
de su primer uso. Pongamos por caso el programa ordena.c en el que hemos implementado
el m´etodo de ordenaci´on por fusi´on: puede que resulte m´as legible definir primero mergesort y
despu´es merge pues, a fin de cuentas, las hemos desarrollado en ese orden. De definirlas as´ı,
necesitar´ıamos declarar anticipadamente merge:
ordena.c
1 #include <stdio.h>
2
3 #define TALLA 100
4
5 void merge(int v[], int inicio1, int final1, int final2); // Declaraci´on anticipada.
6
7 void mergesort(int v[], int inicio, int final)
8 {
9 if (final - inicio == 0)
10 return;
11 else {
12 mergesort ( v, inicio, (inicio+final) / 2 );
13 mergesort ( v, (inicio+final) / 2 + 1, final );
14 merge( v, inicio, (inicio+final) / 2, final ); // Podemos usarla: se ha declarado antes.
15 }
16 }
17
18 void merge(int v[], int inicio1, int final1, int final2) // Y ahora se define.
19 {
20 ...
3.7. Macros
El preprocesador permite definir un tipo especial de funciones que, en el fondo, no lo son: las
macros. Una macro tiene par´ametros y se usa como una funci´on cualquiera, pero las llamadas
no se traducen en verdaderas llamadas a funci´on. Ahora ver´as por qu´e.
Vamos con un ejemplo:
198 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
Prototipo por defecto y declaraci´on anticipada
Si usas una funci´on antes de definirla y no has preparado una declaraci´on anticipada, C
deduce el tipo de cada par´ametro a partir de la forma en la que se le invoca. Este truco
funciona a veces, pero es frecuente que sea fuente de problemas. Considera este ejemplo:
1 int f(int y)
2 {
3 return 1 + g(y);
4 }
5
6 float g(float x)
7 {
8 return x*x;
9 }
En la l´ınea 3 se usa g y a´un no se ha definido. Por la forma de uso, el compilador deduce
que su perfile es int g(int x). Pero, al ver la definici´on, detecta un conflicto.
El problema se soluciona alterando el orden de definici´on de las funciones o, si se prefiere,
mediante una declaraci´on anticipada:
1 float g(float x);
2
3 int f(int y)
4 {
5 return 1 + g(y);
6 }
7
8 float g(float x)
9 {
10 return x*x;
11 }
1 #define CUADRADO(x) x*x
La directiva con la que se define una macro es #define, la misma con la que declar´abamos
constantes. La diferencia est´a en que la macro lleva uno o m´as par´ametros (separados por
comas) encerrados entre par´entesis. Este programa define y usa la macro CUADRADO:
1 #include <stdio.h>
2
3 #define CUADRADO(x) x*x
4
5 int main (void)
6 {
7 printf ("El cuadrado de %d es %dn", 2, CUADRADO(2));
8 return 0;
9 }
El compilador no llega a ver nunca la llamada a CUADRADO. La raz´on es que el preprocesador la
sustituye por su cuerpo, consiguiendo que el compilador vea esta otra versi´on del programa:
1 #include <stdio.h>
2
3
4
5 int main (void)
6 {
7 printf ("El cuadrado de %d es %dn", 2, 2*2);
8 return 0;
9 }
Las macros presentan algunas ventajas frente a las funciones:
Introducci´on a la Programaci´on con C 199
3.7 Macros
Por regla general, son m´as r´apidas que las funciones, pues al no implicar una llamada
a funci´on en tiempo de ejecuci´on nos ahorramos la copia de argumentos en pila y el
salto/retorno a otro lugar del programa.
No obligan a dar informaci´on de tipo acerca de los par´ametros ni del valor de retorno. Por
ejemplo, esta macro devuelve el m´aximo de dos n´umeros, sin importar que sean enteros
o flotantes:
1 #define MAXIMO(A, B) ((A > B) ? A : B)
Pero tienen serios inconvenientes:
La definici´on de la macro debe ocupar, en principio, una sola l´ınea. Si ocupa m´as de una
l´ınea, hemos de finalizar todas menos la ´ultima con el car´acter (()) justo antes del salto
de l´ınea. Inc´omodo.
No puedes definir variables locales.7
No admiten recursi´on.
Son peligros´ısimas. ¿Qu´e crees que muestra por pantalla este programa?:
1 #include <stdio.h>
2
3 #define CUADRADO(x) x*x
4
5 int main (void)
6 {
7 printf ("El cuadrado de 6 es %dn", CUADRADO(3+3));
8 return 0;
9 }
¿36?, es decir, ¿el cuadrado de 6? Pues no es eso lo que obtienes, sino 15. ¿Por qu´e? El
preprocesador sustituye el fragmento CUADRADO(3+3) por. . . ¡3+3*3+3!
El resultado es, efectivamente, 15, y no el que esper´abamos. Puedes evitar este problema
usando par´entesis:
1 #include <stdio.h>
2
3 #define CUADRADO(x) (x)*(x)
4
5 main (void)
6 {
7 printf ("El cuadrado de 6 es %dn", CUADRADO(3+3));
8 return 0;
9 }
Ahora el fragmento CUADRADO(3+3) se sustituye por (3+3)*(3+3), que es lo que espera-
mos. Otro problema resuelto.
No te f´ıes. Ya te hemos dicho que las macros son peligrosas. Sigue estando mal. ¿Qu´e
esperas que calcule 1.0/CUADRADO(3+3)?, ¿el valor de 1/36, es decir, 0.02777. . . ? Te equi-
vocas. La expresi´on 1.0/CUADRADO(3+3) se convierte en 1.0/(3+3)*(3+3), que es 1/6 · 6,
o sea, 1, no 1/36.
La soluci´on pasa por a˜nadir nuevos par´entesis:
1 #include <stdio.h>
2
3 #define CUADRADO(x) ((x)*(x))
4
5 ...
7No del todo cierto, pero no entraremos en detalles.
200 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
¿Ahora s´ı? La expresi´on 1.0/CUADRADO(3+3) se convierte en 1.0/((3+3)*(3+3)), que es
1/36. Pero todav´ıa hay un problema: si ejecutamos este fragmento de c´odigo:
1 i = 3;
2 z = CUADRADO(i++);
la variable se incrementa 2 veces, y no una s´ola. Ten en cuenta que el compilador traduce lo
que ((ve)), y ((ve)) esto:
1 i = 3;
2 z = ((i++)*(i++));
Y este problema no se puede solucionar.
¡Recuerda! Si usas macros, toda precauci´on es poca.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 212 Dise˜na una macro que calcule la tangente de una cantidad de radianes. Puedes usar
las funciones sin y cos de math.h, pero ninguna otra.
· 213 Dise˜na una macro que devuelva el m´ınimo de dos n´umeros, sin importar si son enteros
o flotantes.
· 214 Dise˜na una macro que calcule el valor absoluto de un n´umero, sin importar si es entero
o flotante.
· 215 Dise˜na una macro que decremente una variable entera si y s´olo si es positiva. La macro
devolver´a el valor ya decrementado o inalterado, seg´un convenga.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.8. Otras cuestiones acerca de las funciones
3.8.1. Funciones inline
Los inconvenientes de las macros desaconsejan su uso. Lenguajes como C++ dan soporte a
las macros s´olo por compatibilidad con C, pero ofrecen alternativas mejores. Por ejemplo,
puedes definir funciones inline. Una funci´on inline es como cualquier otra funci´on, s´olo que las
llamadas a ella se gestionan como las llamadas a macros: se sustituye la llamada por el c´odigo
que se ejecutar´ıa en ese caso, o sea, por el cuerpo de la funci´on con los valores que se suministren
para los par´ametros. Las funciones inline presentan muchas ventajas frente a la macros. Entre
ellas, la posibilidad de utilizar variables locales o la no necesidad de utilizar par´entesis alrededor
de toda aparici´on de un par´ametro.
Las funciones inline son tan ´utiles que compiladores como gcc las integran desde hace
a˜nos como extensi´on propia del lenguaje C y han pasado a formar parte del lenguaje C99. Al
compilar un programa C99 como ´este:
1 #include <stdio.h>
2
3 inline int doble(int a)
4 {
5 return a * 2;
6 }
7
8 int main(void)
9 {
10 int i;
11
12 for (i=0; i<10; i++)
13 printf ("%dn", doble(i+1) );
14
15 return 0;
16 }
no se genera c´odigo de m´aquina con 10 llamadas a la funci´on doble. El c´odigo de m´aquina que
se genera es virtualmente id´entico al que se genera para este otro programa equivalente:
Introducci´on a la Programaci´on con C 201
3.8 Otras cuestiones acerca de las funciones
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i;
6
7 for (i=0; i<10; i++)
8 printf ("%dn", ((i+1) * 2) );
9
10 return 0;
11 }
Hay ocasiones, no obstante, en las que el compilador no puede efectuar la sustituci´on de
la llamada a funci´on por su cuerpo. Si la funci´on es recursiva, por ejemplo, la sustituci´on
es imposible. Pero aunque no sea recursiva, el compilador puede juzgar que una funci´on es
excesivamente larga o compleja para que compense efectuar la sustituci´on. Cuando se declara
una funci´on como inline, s´olo se est´a sugiriendo al compilador que efect´ue la sustituci´on, pero
´este tiene la ´ultima palabra sobre si habr´a o no una verdadera llamada a funci´on.
3.8.2. Variables locales static
Hay un tipo especial de variable local: las variables static. Una variable static es invisible
fuera de la funci´on, como cualquier otra variable local, pero recuerda su valor entre diferentes
ejecuciones de la funci´on en la que se declara.
Veamos un ejemplo:
1 #include <stdio.h>
2
3 int turno(void)
4 {
5 static int contador = 0;
6
7 return contador++;
8 }
9
10 int main(void)
11 {
12 int i;
13
14 for (i=0; i<10; i++)
15 printf ("%dn", turno());
16 return 0;
17 }
Si ejecutas el programa aparecer´an por pantalla los n´umeros del 0 al 9. Con cada llamada,
contador devuelve su valor y se incrementa en una unidad, sin olvidar su valor entre llamada y
llamada.
La inicializaci´on de las variables static es opcional: el compilador asegura que empiezan
valiendo 0.
Vamos a volver a escribir el programa que presentamos en el ejercicio 169 para generar
n´umeros primos consecutivos. Esta vez, vamos a hacerlo sin usar una variable global que re-
cuerde el valor del ´ultimo primo generado. Usaremos en su lugar una variable local static:
primos.c primos.c
1 #include <stdio.h>
2
3 int siguienteprimo(void)
4 {
5 static int ultimoprimo = 0;
6 int esprimo;
7 int i;
8
202 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
9 do {
10 ultimoprimo++;
11 esprimo = 1;
12 for (i=2; i<ultimoprimo/2; i++)
13 if (ultimoprimo % i == 0) {
14 esprimo = 0;
15 break;
16 }
17 } while (!esprimo);
18 return ultimoprimo;
19 }
20
21 int main(void)
22 {
23 int i;
24
25 printf ("Los 10 primeros n´umeros primosn");
26 for (i=0; i<10; i++)
27 printf ("%dn", siguienteprimo());
28 return 0;
29 }
Mucho mejor. Si puedes evitar el uso de variables globales, ev´ıtalo. Las variables locales static
pueden ser la soluci´on en bastantes casos.
3.8.3. Paso de funciones como par´ametros
Hay un tipo de par´ametro especial que puedes pasar a una funci´on: ¡otra funci´on!
Veamos un ejemplo. En este fragmento de programa se definen sendas funciones C que
aproximan num´ericamente la integral definida en un intervalo para las funciones matem´aticas
f(x) = x2
y f(x) = x3
, respectivamente:
1 float integra_cuadrado (float a, float b, int n)
2 {
3 int i;
4 float s, x;
5
6 s = 0.0;
7 x = a;
8 for (i=0; i<n; i++) {
9 s += x*x * (b-a)/n;
10 x += (b-a)/n;
11 }
12 return s;
13 }
14
15 float integra_cubo (float a, float b, int n)
16 {
17 int i;
18 float s, x;
19
20 s = 0.0;
21 x = a;
22 for (i=0; i<n; i++) {
23 s += x*x*x * (b-a)/n;
24 x += (b-a)/n;
25 }
26 return s;
27 }
Las dos funciones que hemos definido son b´asicamente iguales. S´olo difieren en su identificador
y en la funci´on matem´atica que integran. ¿No ser´ıa mejor disponer de una ´unica funci´on C,
digamos integra, a la que suministremos como par´ametro la funci´on matem´atica que queremos
integrar? C lo permite:
Introducci´on a la Programaci´on con C 203
3.8 Otras cuestiones acerca de las funciones
1 float integra(float a, float b, int n, float (*f)(float))
2 {
3 int i;
4 float s, x;
5
6 s = 0.0;
7 x = a;
8 for (i=0; i<n; i++) {
9 s += f(x) * (b-a)/n;
10 x += (b-a)/n;
11 }
12 return s;
13 }
Hemos declarado un cuarto par´ametro que es de tipo puntero a funci´on. Cuando llamamos a
integra, el cuarto par´ametro puede ser el identificador de una funci´on que reciba un float y
devuelva un float:
integra.c integra.c
1 #include <stdio.h>
2
3 float integra(float a, float b, int n, float (*f)(float))
4 {
5 int i;
6 float s, x;
7
8 s = 0.0;
9 x = a;
10 for (i=0; i<n; i++) {
11 s += f(x) * (b-a)/n;
12 x += (b-a)/n;
13 }
14 return s;
15 }
16
17 float cuadrado(float x)
18 {
19 return x*x;
20 }
21
22 float cubo(float x)
23 {
24 return x*x*x;
25 }
26
27 int main(void)
28 {
29 printf ("Integral 1: %fn", integra(0.0, 1.0, 10, cuadrado ));
30 printf ("Integral 2: %fn", integra(0.0, 1.0, 10, cubo ));
31 return 0;
32 }
La forma en que se declara un par´ametro del tipo ((puntero a funci´on)) resulta un tanto
complicada. En nuestro caso, lo hemos declarado as´ı: float (*f)(float). El primer float indica
que la funci´on devuelve un valor de ese tipo. El (*f) indica que el par´ametro f es un puntero a
funci´on. Y el float entre par´entesis indica que la funci´on trabaja con un par´ametro de tipo float.
Si hubi´esemos necesitado trabajar con una funci´on que recibe un float y un int, hubi´esemos
escrito float (*f)(float, int) en la declaraci´on del par´ametro.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 216 ¿Puedes usar la funci´on integra para calcular la integral definida de la funci´on ma-
tem´atica sin(x)? ¿C´omo?
204 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
· 217 Dise˜na una funci´on C capaz de calcular
b
i=a
f(i),
siendo f una funci´on matem´atica cualquiera que recibe un entero y devuelve un entero.
· 218 Dise˜na una funci´on C capaz de calcular
b
i=a
d
j=c
f(i, j),
siendo f una funci´on matem´atica cualquiera que recibe dos enteros y devuelve un entero.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.9. M´odulos, bibliotecas y unidades de compilaci´on
Cuando te enfrentas a la escritura de un programa largo, individualmente o en equipo, te
resultar´a virtualmente imposible escribirlo en un ´unico fichero de texto. Resulta m´as pr´actico
agrupar diferentes partes del programa en ficheros independientes. Cada fichero puede, por
ejemplo, agrupar las funciones, registros y constantes propias de cierto tipo de c´alculos.
Proceder as´ı tiene varias ventajas:
Mejora la legibilidad del c´odigo (cada fichero es relativamente breve y agrupa tem´aticamente
las funciones, registros y constantes).
La compilaci´on es m´as r´apida (cuando se modifica un fichero, s´olo es necesario compilar
ese fichero).
Y, quiz´a lo m´as importante, permite reutilizar c´odigo. Es un beneficio a medio y largo pla-
zo. Si, por ejemplo, te dedicas a programar videojuegos tridimensionales, ver´as que todos
ellos comparten ciertas constantes, registros y funciones definidas por t´ı o por otros pro-
gramadores: tipos de datos para modelar puntos, pol´ıgonos, texturas, etc´etera; funciones
que los manipulan, visualizan, leen/escriben en disco, etc´etera. Puedes definir estos ele-
mentos en un fichero y utilizarlo en cuantos programas desees. Alternativamente, podr´ıas
copiar-y-pegar las funciones, constantes y registros que uno necesita en cada programa,
pero no es conveniente en absoluto: corregir un error en una funci´on obligar´ıa a editar
todos los programas en los que se peg´o; por contra, si est´a en un solo fichero, basta con
corregir la definici´on una sola vez.
C permite escribir un programa como una colecci´on de unidades de compilaci´on. El con-
cepto es similar al de los m´odulos Python: cada unidad agrupa definiciones de variables, tipos,
constantes y funciones orientados a resolver cierto tipo de problemas. Puedes compilar indepen-
dientemente cada unidad de compilaci´on (de ah´ı el nombre) de modo que el compilador genere
un fichero binario para cada una de ellas. El enlazador se encarga de unir en una ´ultima etapa
todas las unidades compiladas para crear un ´unico fichero ejecutable.
Lo mejor ser´a que aprendamos sobre unidades de compilaci´on escribiendo una muy sencilla:
un m´odulo en el que se define una funci´on que calcula el m´aximo de dos n´umero enteros.
El fichero que corresponde a esta unidad de compilaci´on se llamar´a extremos.c. He aqu´ı su
contenido:
extremos.c extremos.c
1 int maximo(int a, int b)
2 {
3 if (a > b)
4 return a;
5 else
6 return b;
7 }
El programa principal se escribir´a en otro fichero llamado principal.c. Dicho programa lla-
mar´a a la funci´on maximo:
Introducci´on a la Programaci´on con C 205
3.9 M´odulos, bibliotecas y unidades de compilaci´on
principal 1.c E principal.c E
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int x, y;
6
7 printf ("Dame un n´umero: ");
8 scanf ("%d", &x);
9 printf ("Dame otro: ");
10 scanf ("%d", &y);
11 printf ("El m´aximo es %dn", maximo(x, y));
12 return 0;
13 }
Hemos marcado el programa como incorrecto. ¿Por qu´e? Ver´as, estamos usando una funci´on,
maximo, que no est´a definida en el fichero principal.c. ¿C´omo sabe el compilador cu´antos
par´ametros recibe dicha funci´on?, ¿y el tipo de cada par´ametro?, ¿y el tipo del valor de retorno?
El compilador se ve obligado a generar c´odigo de m´aquina para llamar a una funci´on de la que
no sabe nada. Mala cosa.
¿C´omo se resuelve el problema? Puedes declarar la funci´on sin definirla, es decir, puedes
declarar el aspecto de su cabecera (lo que denominamos su prototipo) e indicar que es una
funci´on definida externamente:
principal 2.c principal.c
1 #include <stdio.h>
2
3 extern int maximo(int a, int b);
4
5 int main(void)
6 {
7 int x, y;
8
9 printf ("Dame un n´umero: ");
10 scanf ("%d", &x);
11 printf ("Dame otro: ");
12 scanf ("%d", &y);
13 printf ("El m´aximo es %dn", maximo(x, y));
14 return 0;
15 }
El prototipo contiene toda la informaci´on ´util para efectuar la llamada a la funci´on, pero no
contiene su cuerpo: la cabecera acaba con un punto y coma. F´ıjate en que la declaraci´on del
prototipo de la funci´on maximo empieza con la palabra clave extern. Con ella se indica al
compilador que maximo est´a definida en alg´un m´odulo ((externo)). Tambi´en puedes indicar con
extern que una variable se define en otro m´odulo.
Puedes compilar el programa as´ı:
$ gcc extremos.c -c
$ gcc principal.c -c
$ gcc principal.o extremos.o -o principal
La compilaci´on necesita tres pasos: uno por cada unidad de compilaci´on y otro para enlazar.
1. El primer paso (gcc extremos.c -c) traduce a c´odigo de m´aquina el fichero o unidad de
compilaci´on extremos.c. La opci´on -c indica al compilador que extremos.c es un m´odulo
y no define a la funci´on main. El resultado de la compilaci´on se deja en un fichero llamado
extremos.o. La extensi´on ((.o)) abrevia el t´ermino ((object code)), es decir, ((c´odigo objeto)).
Los ficheros con extensi´on ((.o)) contienen el c´odigo de m´aquina de nuestras funciones8
,
pero no es directamente ejecutable.
2. El segundo paso (gcc principal.c -c) es similar al primero y genera el fichero principal.o
a partir de principal.c.
8. . . pero no s´olo eso: tambi´en contienen otra informaci´on, como la denominada tabla de s´ımbolos.
206 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
3. El tercer paso (gcc principal.o extremos.o -o principal) es especial. El compilador
recibe dos ficheros con extensi´on ((.o)) y genera un ´unico fichero ejecutable, llamado prin-
cipal. Este ´ultimo paso se encarga de enlazar las dos unidades compiladas para generar
el fichero ejecutable.
Por enlazar entendemos que las llamadas a funciones cuyo c´odigo de m´aquina era desco-
nocido (estaba en otra unidad de compilaci´on) se traduzcan en ((saltos)) a las direcciones
en las que se encuentran los subprogramas de c´odigo m´aquina correspondientes (y que
ahora se conocen).
Aqu´ı tienes un diagrama que ilustra el proceso:
Enlazador principal
Paso 3
extremos.c Compilador extremos.o
Paso 1
principal.c Compilador principal.o
Paso 2
Puedes ahorrarte un paso fundiendo los dos ´ultimos en uno s´olo. As´ı:
$ gcc extremos.c -c
$ gcc principal.c extremos.o -o principal
Este diagrama muestra todos los pasos del proceso a los que aludimos:
principal.c Compilador Enlazador principal
Paso 2
extremos.c Compilador extremos.o
Paso 1
Para conseguir un programa ejecutable es necesario que uno de los m´odulos (¡pero s´olo uno
de ellos!) defina una funci´on main. Si ning´un m´odulo define main o si main se define en m´as
de un m´odulo, el enlazador protestar´a y no generar´a fichero ejecutable alguno.
3.9.1. Declaraci´on de prototipos en cabeceras
Hemos resuelto el problema de gestionar diferentes unidades de compilaci´on, pero la soluci´on de
tener que declarar el prototipo de cada funci´on en toda unidad de compilaci´on que la usa no es
muy buena. Hay una mejor: definir un fichero de cabecera. Los ficheros de cabecera agrupan las
declaraciones de funciones (y cualquier otro elemento) definidos en un m´odulo. Las cabeceras
son ficheros con extensi´on ((.h)) (es un convenio: la ((h)) es abreviatura de ((header))).
Nuestra cabecera ser´a este fichero:
extremos.h extremos.h
1 extern int maximo(int a, int b);
Para incluir la cabecera en nuestro programa, escribiremos una nueva directiva #include:
principal.c principal.c
1 #include <stdio.h>
2 #include "extremos.h"
3
4 int main(void)
5 {
6 int x, y;
7
Introducci´on a la Programaci´on con C 207
3.9 M´odulos, bibliotecas y unidades de compilaci´on
Documentaci´on y cabeceras
Es importante que documentes bien los ficheros de cabecera, pues es frecuente que los
programadores que usen tu m´odulo lo consulten para hacerse una idea de qu´e ofrece.
Nuestro m´odulo podr´ıa haberse documentado as´ı:
extremos.h
1 /*******************************************************
2 * M´odulo: extremos
3 *
4 * Prop´osito: funciones para c´alculo de valores m´aximos
5 * y m´ınimos.
6 *
7 * Autor: A. U. Thor.
8 *
9 * Fecha: 12 de enero de 1997
10 *
11 * Estado: Incompleto. Falta la funci´on minimo.
12 *******************************************************/
14
15 extern int maximo(int a, int b);
16 /* Calcula el m´aximo de dos n´umero enteros a y b. */
¿Y por qu´e los programadores no miran directamente el fichero .c en lugar del .h
cuando quieren consultar algo? Por varias razones. Una de ellas es que, posiblemente, el
.c no est´e accesible. Si el m´odulo es un producto comercial, probablemente s´olo les hayan
vendido el m´odulo ya compilado (el fichero .o) y el fichero de cabecera. Pero incluso si
se tiene acceso al .c, puede ser preferible ver el .h. El fichero .c puede estar plagado de
detalles de implementaci´on, funciones auxiliares, variables para uso interno, etc., que hacen
engorrosa su lectura. El fichero de cabecera contiene una somera declaraci´on de cada uno
de los elementos del m´odulo que se ((publican)) para su uso en otros m´odulos o programas,
as´ı que es una especie de resumen del .c.
8 printf ("Dame un n´umero: ");
9 scanf ("%d", &x);
10 printf ("Dame otro: ");
11 scanf ("%d", &y);
12 printf ("El m´aximo es %dn", maximo(x, y));
13 return 0;
14 }
La ´unica diferencia con respecto a otros #include que ya hemos usado estriba en el uso de
comillas dobles para encerrar el nombre del fichero, en lugar de los caracteres ((<)) y ((>)). Con
ello indicamos al preprocesador que el fichero extremos.h se encuentra en nuestro directorio
activo. El preprocesador se limita a sustituir la l´ınea en la que aparece #include "extremos.h"
por el contenido del fichero. En un ejemplo tan sencillo no hemos ganado mucho, pero si el
m´odulo extremos.o contuviera muchas funciones, con s´olo una l´ınea habr´ıamos conseguido
((importarlas)) todas.
Aqu´ı tienes una actualizaci´on del gr´afico que muestra el proceso completo de compilaci´on:
principal.c Preprocesador Compilador Enlazador principal
extremos.c Compilador extremos.o
extremos.h
208 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
Bibliotecas
Ya has usado funciones y datos predefinidos, como las funciones y las constantes ma-
tem´aticas. Hemos hablado entonces del uso de la biblioteca matem´atica. ¿Por qu´e
((biblioteca)) y no ((m´odulo))? Una biblioteca es m´as que un m´odulo: es un conjunto de
m´odulos.
Cuando se tiene una pl´eyade de ficheros con extensi´on ((.o)), conviene empaquetarlos en
uno solo con extensi´on ((.a)) (por ((archive))). Los ficheros con extensi´on ((.a)) son similares
a los ficheros con extensi´on ((.tar)): meras colecciones de ficheros. De hecho, ((tar)) (tape
archiver) es una evoluci´on de ((.ar)) (por ((archiver))), el programa con el que se manipulan
los ficheros con extensi´on ((.a)).
La biblioteca matem´atica, por ejemplo, agrupa un mont´on de m´odulos. En un sistema
Linux se encuentra en el fichero /usr/lib/libm.a y puedes consultar su contenido con esta
orden:
$ ar tvf /usr/lib/libm.a
rw-r--r-- 0/0 29212 Sep 9 18:17 2002 k_standard.o
rw-r--r-- 0/0 8968 Sep 9 18:17 2002 s_lib_version.o
rw-r--r-- 0/0 9360 Sep 9 18:17 2002 s_matherr.o
rw-r--r-- 0/0 8940 Sep 9 18:17 2002 s_signgam.o
...
rw-r--r-- 0/0 1152 Sep 9 18:17 2002 slowexp.o
rw-r--r-- 0/0 1152 Sep 9 18:17 2002 slowpow.o
Como puedes ver, hay varios ficheros con extensi´on ((.o)) en su interior. (S´olo te mostra-
mos el principio y el final del resultado de la llamada, pues hay un total de ¡395 ficheros!)
Cuando usas la biblioteca matem´atica compilas as´ı:
$ gcc programa.c -lm -o programa
o, equivalentemente, as´ı:
$ gcc programa.c /usr/lib/libm.a -o programa
En el segundo caso hacemos expl´ıcito el nombre de la biblioteca en la que se encuentran
las funciones matem´aticas. El enlazador no s´olo sabe tratar ficheros con extensi´on ((.o)):
tambi´en sabe buscarlos en los de extensi´on ((.a)).
En cualquier caso, sigue siendo necesario que las unidades de compilaci´on conozcan el
perfil de las funciones que usan y est´an definidas en otros m´odulos o bibliotecas. Por eso
inclu´ımos, cuando conviene, el fichero math.h en nuestros programas.
Hay infinidad de bibliotecas que agrupan m´odulos con utilidades para diferentes campos
de aplicaci´on: resoluci´on de problemas matem´aticos, dise˜no de videojuegos, reproducci´on
de m´usica, etc. Algunas son c´odigo abierto, en cuyo caso se distribuyen con los ficheros de
extensi´on ((.c)), los ficheros de extensi´on ((.h)) y alguna utilidad para facilitar la compilaci´on
(un makefile). Cuando son comerciales es frecuente que se mantenga el c´odigo fuente en
privado. En tal caso, se distribuye el fichero con extensi´on ((.a)) (o una colecci´on de ficheros
con extensi´on ((.o))) y uno o m´as ficheros con extensi´on ((.h)).
3.9.2. Declaraci´on de variables en cabeceras
No s´olo puedes declarar funciones en los ficheros de cabecera. Tambi´en puedes definir constantes,
variables y registros.
Poco hay que decir sobre las constantes. Basta con que las definas con #define en el fichero
de cabecera. Las variables, sin embargo, s´ı plantean un problema. Este m´odulo, por ejemplo,
declara una variable entera en mimodulo.c:
mimodulo.c
1 int variable;
Si deseamos que otras unidades de compilaci´on puedan acceder a esa variable, tendremos que
incluir su declaraci´on en la cabecera. ¿C´omo? Una primera idea es poner, directamente, la
declaraci´on as´ı:
E mimodulo.h E
1 int variable;
Introducci´on a la Programaci´on con C 209
3.9 M´odulos, bibliotecas y unidades de compilaci´on
Pero es incorrecta. El problema radica en que cuando incluyamos la cabecera mimodulo.h en
nuestro programa, se insertar´a la l´ınea int variable;, sin m´as, as´ı que se estar´a definiendo una
nueva variable con el mismo identificador que otra. Y declarar dos variables con el mismo
identificador es un error.
Quien detecta el error es el enlazador: cuando vaya a generar el programa ejecutable, encon-
trar´a que hay dos objetos que tienen el mismo identificador, y eso est´a prohibido. La soluci´on es
sencilla: preceder la declaraci´on de variable en la cabecera mimodulo.h con la palabra reservada
extern:
mimodulo.h
1 extern int variable;
De ese modo, cuando se compila un programa que incluye a mimodulo.h, el compilador sabe
que variable es de tipo int y que est´a definida en alguna unidad de compilaci´on, por lo que no
la crea por segunda vez.
3.9.3. Declaraci´on de registros en cabeceras
Finalmente, puedes declarar tambi´en registros en las cabeceras. Como los programas que cons-
truiremos son sencillos, no se plantear´a problema alguno con la definici´on de registros: basta con
que pongas su declaraci´on en la cabecera, sin m´as. Pero si tu programa incluye dos cabeceras
que, a su vez, incluyen ambas a una tercera donde se definen constantes o registros, puedes
tener problemas. Un ejemplo ilustrar´a mejor el tipo de dificultades al que nos enfrentamos.
Supongamos que un fichero a.h define un registro:
a.h
1 // Cabecera a.h
2 struct A {
3 int a;
4 };
5 // Fin de cabecera a.h
Ahora, los ficheros b.h y c.h incluyen a a.h y declaran la existencia de sendas funciones:
b.h
1 // Cabecera b.h
2 #include "a.h"
3
4 int funcion_de_b_punto_h(int x);
5 // Fin de cabecera b.h
c.h
1 // Cabecera c.h
2 #include "a.h"
3
4 int funcion_de_c_punto_h(int x);
5 // Fin de cabecera c.h
Y, finalmente, nuestro programa incluye tanto a b.h como a c.h:
programa.c
1 #include <stdio.h>
2
3 #include "b.h"
4
5 #include "c.h"
6
7 int main(void)
8 {
9 ...
10 }
El resultado es que el a.h acaba quedando incluido ¡dos veces! Tras el paso de programa.c por
el preprocesador, el compilador se enfrenta, a este texto:
210 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones
programa.c
1 #include <stdio.h>
2
3 // Cabecera b.h.
4 // Cabecera a.h.
5 struct A {
6 int a;
7 };
8 // Fin de cabecera a.h.
9
10 int funcion_de_b_punto_h(int x);
11 // Fin de cabecera b.h.
12
13 // Cabecera c.h.
14 // Cabecera a.h.
15 struct A {
16 int a;
17 };
18 // Fin de cabecera a.h.
19
20 int funcion_de_c_punto_h(int x);
21 // Fin de cabecera c.h.
22
23 int main(void)
24 {
25 ...
26 }
El compilador encuentra, por tanto, la definici´on de struct A por duplicado, y nos avisa del
((error)). No importa que las dos veces se declare de la misma forma: C lo considera ilegal. El
problema puede resolverse reescribiendo a.h (y, en general, cualquier fichero cabecera) as´ı:
1 // Cabecera de a.h
2 #ifndef A_H
3 #define A_H
4
5 struct A {
6 int a;
7 };
8
9 #endif
10 // Fin de cabecera de a.h
Las directivas #ifndef/#endif marcan una zona de ((c´odigo condicional)). Se interpretan as´ı:
((si la constante A_H no est´a definida, entonces incluye el fragmento hasta el #endif, en caso
contrario, s´altate el texto hasta el #endif)). O sea, el compilador ver´a o no lo que hay entre
las l´ıneas 3 y 8 en funci´on de si existe o no una determinada constante. No debes confundir
estas directivas con una sentencia if: no lo son. La sentencia if permite ejecutar o no un bloque
de sentencias en funci´on de que se cumpla o no una condici´on en tiempo de ejecuci´on. Las
directivas presentadas permiten que el compilador vea o no un fragmento arbitrario de texto
en funci´on de si existe o no una constante en tiempo de compilaci´on.
Observa que lo primero que se hace en ese fragmento de programa es definir la constante
A_H (l´ınea 3). La primera vez que se incluya la cabecera a.h no estar´a a´un definida A_H, as´ı que
se incluir´an las l´ıneas 3–8. Uno de los efectos ser´a que A_H pasar´a a estar definida. La segunda
vez que se incluya la cabecera a.h, A_H ya estar´a definida, as´ı que el compilador no ver´a por
segunda vez la definici´on de struct A.
El efecto final es que la definici´on de struct A s´olo se ve una vez. He aqu´ı lo que resulta de
programa.c tras su paso por el preprocesador:
programa.c
1 #include <stdio.h>
2
3 // Cabecera b.h.
4 // Cabecera a.h.
Introducci´on a la Programaci´on con C 211
3.9 M´odulos, bibliotecas y unidades de compilaci´on
5 struct A {
6 int a;
7 };
8 // Fin de cabecera a.h.
9
10 int funcion_de_b_punto_h(int x);
11 // Fin de cabecera b.h.
12
13 // Cabecera c.h.
14 // Cabecera a.h.
15 // Fin de cabecera a.h.
16
17 int funcion_de_c_punto_h(int x);
18 // Fin de cabecera c.h.
19
20 int main(void)
21 {
22 ...
23 }
La segunda inclusi´on de a.h no ha supuesto el copiado del texto guardado entre directivas
#ifndef/#endif. Ingenioso, ¿no?
212 Introducci´on a la Programaci´on con C
Cap´ıtulo 4
Estructuras de datos: memoria
din´amica
La Reina se puso congestionada de furia, y, tras lanzarle una mirada felina, empez´o a
gritar: ((¡Que le corten la cabeza! ¡Que le corten. . . !)).
Lewis Carroll, Alicia en el Pa´ıs de las Maravillas.
Vimos en el cap´ıtulo 2 que los vectores de C presentaban un serio inconveniente con respecto a
las listas de Python: su tama˜no deb´ıa ser fijo y conocido en tiempo de compilaci´on, es decir, no
pod´ıamos alargar o acortar los vectores para que se adaptaran al tama˜no de una serie de datos
durante la ejecuci´on del programa. C permite una gesti´on din´amica de la memoria, es decir,
solicitar memoria para albergar el contenido de estructuras de datos cuyo tama˜no exacto no
conocemos hasta que se ha iniciado la ejecuci´on del programa. Estudiaremos aqu´ı dos formas
de superar las limitaciones de tama˜no que impone el C:
mediante vectores cuyo tama˜no se fija en tiempo de ejecuci´on,
y mediante registros enlazados, tambi´en conocidos como listas enlazadas (o, simplemente,
listas).
Ambas aproximaciones se basan en el uso de punteros y cada una de ellas presenta diferentes
ventajas e inconvenientes.
4.1. Vectores din´amicos
Sabemos definir vectores indicando su tama˜no en tiempo de compilaci´on:
1 #define TALLA 10
2
3 int a[TALLA];
Pero, ¿y si no sabemos a priori cu´antos elementos debe albergar el vector?1
Por lo estudiado
hasta el momento, podemos definir TALLA como el n´umero m´as grande de elementos posible,
el n´umero de elementos para el peor de los casos. Pero, ¿y si no podemos determinar un
n´umero m´aximo de elementos? Aunque pudi´eramos, ¿y si ´este fuera tan grande que, en la
pr´actica, supusiera un despilfarro de memoria intolerable para situaciones normales? Imagina
una aplicaci´on de agenda telef´onica personal que, por si acaso, reserva 100000 entradas en un
vector. Lo m´as probable es que un usuario convencional no gaste m´as de un centenar. Estaremos
desperdiciando, pues, unas 99900 celdas del vector, cada una de las cuales puede consistir en
un centenar de bytes. Si todas las aplicaciones del ordenador se dise˜naran as´ı, la memoria
disponible se agotar´ıa rapid´ısimamente.
1En la secci´on 3.5.3 vimos c´omo definir vectores locales cuya talla se decide al ejecutar una funci´on: lo que
denominamos ((vectores de longitud variable)). Nos proponemos dos objetivos: por una parte, poder redimensionar
vectores globales; y, por otro, vamos a permitir que un vector crezca y decrezca en tama˜no cuantas veces
queramos. Los ((vectores de longitud variable)) que estudiamos en su momento son inapropiados para cualquiera
de estos dos objetivos.
Introducci´on a la Programaci´on con C 213
4.1 Vectores din´amicos
4.1.1. malloc, free y NULL
Afortunadamente, podemos definir, durante la ejecuci´on del programa, vectores cuyo tama˜no
es exactamente el que el usuario necesita. Utilizaremos para ello dos funciones de la biblioteca
est´andar (disponibles incluyendo la cabecera stdlib.h):
malloc (abreviatura de ((memory allocate)), que podemos traducir por ((reservar memo-
ria))): solicita un bloque de memoria del tama˜no que se indique (en bytes);
free (que en ingl´es significa ((liberar))): libera memoria obtenida con malloc, es decir, la
marca como disponible para futuras llamadas a malloc.
Para hacernos una idea de c´omo funciona, estudiemos un ejemplo:
vector dinamico.c vector dinamico.c
1 #include <stdlib.h>
2 #include <stdio.h>
3
4 int main(void)
5 {
6 int * a;
7 int talla, i;
8
9 printf ("N´umero de elementos: "); scanf ("%d", &talla);
10 a = malloc( talla * sizeof(int) );
11 for (i=0; i<talla; i++)
12 a[i] = i;
13 free(a);
14 a = NULL;
15
16 return 0;
17 }
F´ıjate en c´omo se ha definido el vector a (l´ınea 6): como int * a, es decir, como puntero a entero.
No te dejes enga˜nar: no se trata de un puntero a un entero, sino de un puntero a una secuencia de
enteros. Ambos conceptos son equivalentes en C, pues ambos son meras direcciones de memoria.
La variable a es un vector din´amico de enteros, pues su memoria se obtiene din´amicamente,
esto es, en tiempo de ejecuci´on y seg´un convenga a las necesidades. No sabemos a´un cu´antos
enteros ser´an apuntados por a, ya que el valor de talla no se conocer´a hasta que se ejecute el
programa y se lea por teclado.
Sigamos. La l´ınea 10 reserva memoria para talla enteros y guarda en a la direcci´on de
memoria en la que empiezan esos enteros. La funci´on malloc presenta un prototipo similar a
´este:
stdlib.h
...
void * malloc(int bytes);
...
Es una funci´on que devuelve un puntero especial, del tipo de datos void *. ¿Qu´e significa
void *? Significa ((puntero a cualquier tipo de datos)), o sea, ((direcci´on de memoria)), sin m´as.
La funci´on malloc no se usa s´olo para reservar vectores din´amicos de enteros: puedes reservar
con ella vectores din´amicos de cualquier tipo base. Analicemos ahora el argumento que pasamos
a malloc. La funci´on espera recibir como argumento un n´umero entero: el n´umero de bytes que
queremos reservar. Si deseamos reservar talla valores de tipo int, hemos de solicitar memoria
para talla * sizeof(int) bytes. Recuerda que sizeof(int) es la ocupaci´on en bytes de un dato
de tipo int (y que estamos asumiendo que es de 4).
Si el usuario decide que talla valga, por ejemplo, 5, se reservar´a un total de 20 bytes y la
memoria quedar´a as´ı tras ejecutar la l´ınea 10:
a
0 1 2 3 4
214 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
Es decir, se reserva suficiente memoria para albergar 5 enteros.
Como puedes ver, las l´ıneas 11–12 tratan a a como si fuera un vector de enteros cualquiera.
Una vez has reservado memoria para un vector din´amico, no hay diferencia alguna entre ´el y un
vector est´atico desde el punto de vista pr´actico. Ambos pueden indexarse (l´ınea 12) o pasarse
como argumento a funciones que admiten un vector del mismo tipo base.
Aritm´etica de punteros
Una curiosidad: el acceso indexado a[0] es equivalente a *a. En general, a[i] es equivalente
a *(a+i), es decir, ambas son formas de expresar el concepto ((accede al contenido de la
direcci´on a con un desplazamiento de i veces el tama˜no del tipo base)). La sentencia de
asignaci´on a[i] = i podr´ıa haberse escrito como *(a+i) = i. En C es posible sumar o restar
un valor entero a un puntero. El entero se interpreta como un desplazamiento dado en
unidades ((tama˜no del tipo base)) (en el ejemplo, 4 bytes, que es el tama˜no de un int). Es
lo que se conoce por aritm´etica de punteros.
La aritm´etica de punteros es un punto fuerte de C, aunque tambi´en tiene sus detractores:
resulta sencillo provocar accesos incorrectos a memoria si se usa mal.
Finalmente, la l´ınea 13 del programa libera la memoria reservada y la l´ınea 14 guarda en a
un valor especial: NULL. La funci´on free tiene un prototipo similar a ´este:
stdlib.h
...
void free(void * puntero);
...
Como ves, free recibe un puntero a cualquier tipo de datos: la direcci´on de memoria en la que
empieza un bloque previamente obtenido con una llamada a malloc. Lo que hace free es liberar
ese bloque de memoria, es decir, considerar que pasa a estar disponible para otras posibles
llamadas a malloc. Es como cerrar un fichero: si no necesito un recurso, lo libero para que otros
lo puedan aprovechar.2
Puedes aprovechar as´ı la memoria de forma ´optima.
Recuerda: tu programa debe efectuar una llamada a free por cada llamada a malloc. Es muy
importante.
Conviene que despu´es de hacer free asignes al puntero el valor NULL, especialmente si la
variable sigue ((viva)) durante bastante tiempo. NULL es una constante definida en stdlib.h. Si
un puntero vale NULL, se entiende que no apunta a un bloque de memoria. Gr´aficamente, un
puntero que apunta a NULL se representa as´ı:
a
Liberar memoria no cambia el valor del puntero
La llamada a free libera la memoria apuntada por un puntero, pero no modifica el valor de
la variable que se le pasa. Imagina que un bloque de memoria de 10 enteros que empieza
en la direcci´on 1000 es apuntado por una variable a de tipo int *, es decir, imagina que a
vale 1000. Cuando ejecutamos free(a), ese bloque se libera y pasa a estar disponible para
eventuales llamadas a malloc, pero ¡a sigue valiendo 1000! ¿Por qu´e? Porque a se ha pasado
a free por valor, no por referencia, as´ı que free no tiene forma de modificar el valor de a. Es
recomendable que asignes a a el valor NULL despu´es de una llamada a free, pues as´ı haces
expl´ıcito que la variable a no apunta a nada.
Recuerda, pues, que es responsabilidad tuya y que conviene hacerlo: asigna
expl´ıcitamente el valor NULL a todo puntero que no apunte a memoria reservada.
La funci´on malloc puede fallar por diferentes motivos. Podemos saber cu´ando ha fallado
porque malloc lo notifica devolviendo el valor NULL. Imagina que solicitas 2 megabytes de
memoria en un ordenador que s´olo dispone de 1 megabyte. En tal caso, la funci´on malloc
devolver´a el valor NULL para indicar que no pudo efectuar la reserva de memoria solicitada.
2Y, como en el caso de un fichero, si no lo liberas t´u expl´ıcitamente, se libera autom´aticamente al finalizar
la ejecuci´on del programa. A´un as´ı, te exigimos disciplina: obl´ıgate a liberarlo t´u mismo tan pronto dejes de
necesitarlo.
Introducci´on a la Programaci´on con C 215
4.1 Vectores din´amicos
Los programas correctamente escritos deben comprobar si se pudo obtener la memoria so-
licitada y, en caso contrario, tratar el error.
1 a = malloc(talla * sizeof(int));
2 if (a == NULL) {
3 printf ("Error: no hay memoria suficienten");
4 }
5 else {
6 ...
7 }
Es posible (y una forma de expresi´on idiom´atica de C) solicitar la memoria y comprobar si se
pudo obtener en una ´unica l´ınea (presta atenci´on al uso de par´entesis, es importante):
1 if ( (a = malloc(talla * sizeof(int))) == NULL) {
2 printf ("Error: no hay memoria suficienten");
3 }
4 else {
5 ...
6 }
Nuestros programas, sin embargo, no incluir´an esta comprobaci´on. Estamos aprendiendo a pro-
gramar y sacrificaremos las comprobaciones como ´esta en aras de la legibilidad de los programas.
Pero no lo olvides: los programas con un acabado profesional deben comprobar y tratar posibles
excepciones, como la no existencia de suficiente memoria.
Fragmentaci´on de la memoria
Ya hemos dicho que malloc puede fracasar si se solicita m´as memoria de la disponible en
el ordenador. Parece l´ogico pensar que en un ordenador con 64 megabytes, de los que el
sistema operativo y los programas en ejecuci´on han consumido, digamos, 16 megabytes,
podamos solicitar un bloque de hasta 48 megabytes. Pero eso no est´a garantizado. Imagina
que los 16 megabytes ya ocupados no est´an dispuestos contiguamente en la memoria sino
que, por ejemplo, se alternan con fragmentos de memoria libre de modo que, de cada cuatro
megabytes, uno est´a ocupado y tres est´an libres, como muestra esta figura:
En tal caso, el bloque de memoria m´as grande que podemos obtener con malloc es de ¡s´olo
tres megabytes!
Decimos que la memoria est´a fragmentada para referirnos a la alternancia de bloques
libres y ocupados que limita su disponibilidad. La fragmentaci´on no s´olo limita el m´aximo
tama˜no de bloque que puedes solicitar, adem´as, afecta a la eficiencia con la que se ejecutan
las llamadas a malloc y free.
Tambi´en puedes usar NULL para inicializar punteros y dejar expl´ıcitamente claro que no se
les ha reservado memoria.
vector dinamico 1.c vector dinamico.c
1 #include <stdlib.h>
2 #include <stdio.h>
3
4 int main(void)
5 {
6 int * a = NULL;
7 int talla, i;
8
9 printf ("N´umero de elementos: "); scanf ("%d", &talla);
10 a = malloc( talla * sizeof(int) );
11 for (i=0; i<talla; i++)
12 a[i] = i;
13 free(a);
14 a = NULL;
15
216 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
16 return 0;
17 }
Aritm´etica de punteros y recorrido de vectores
La aritm´etica de punteros da lugar a expresiones idiom´aticas de C que deber´ıas saber leer.
F´ıjate en este programa:
vector dinamico 2.c vector dinamico.c
1 #include <stdlib.h>
2 #include <stdio.h>
3
4 int main(void)
5 {
6 int * a = NULL;
7 int talla, i;
8 int * p;
9
10 printf ("N´umero de elementos: "); scanf ("%d", &talla);
11 a = malloc( talla * sizeof(int) );
12 for (i=0, p=a; i<talla; i++, p++)
13 *p = i;
14 free(a);
15 a = NULL;
16
17 return 0;
18 }
El efecto del bucle es inicializar el vector con la secuencia 0, 1, 2. . . El puntero p empieza
apuntando a donde a, o sea, al principio del vector. Con cada autoincremento, p++, pasa a
apuntar a la siguiente celda. Y la sentencia *p = i asigna al lugar apuntado por p el valor i.
4.1.2. Algunos ejemplos
Es hora de poner en pr´actica lo aprendido desarrollando un par de ejemplos.
Creaci´on de un nuevo vector con una selecci´on, de talla desconocida, de elementos
de otro vector
Empezaremos por dise˜nar una funci´on que recibe un vector de enteros, selecciona aquellos cuyo
valor es par y los devuelve en un nuevo vector cuya memoria se solicita din´amicamente.
1 int * selecciona_pares(int a[], int talla)
2 {
3 int i, j, numpares = 0;
4 int * pares;
5
6 // Primero hemos de averiguar cu´antos elementos pares hay en a.
7 for (i=0; i<talla; i++)
8 if (a[i] % 2 == 0)
9 numpares++;
10
11 // Ahora podemos pedir memoria para ellos.
12 pares = malloc( numpares * sizeof(int) );
13
14 // Y, finalmente, copiar los elementos pares en la zona de memoria solicitada.
15 j = 0;
16 for (i=0; i<talla; i++)
17 if (a[i] % 2 == 0)
18 pares[j++] = a[i];
19
Introducci´on a la Programaci´on con C 217
4.1 Vectores din´amicos
20 return pares;
21 }
Observa que devolvemos un dato de tipo int *, es decir, un puntero a entero; bueno, en realidad
se trata de un puntero a una secuencia de enteros (recuerda que son conceptos equivalentes en
C). Es la forma que tenemos de devolver vectores desde una funci´on.
Este programa, por ejemplo, llama a selecciona_pares:
pares.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <time.h>
4
5 #define TALLA 10
6
.
.
.
27 }
28
29 int main(void)
30 {
31 int vector[TALLA], i;
32 int * seleccion;
33
34 // Llenamos el vector con valores aleatorios.
35 srand(time(0));
36 for (i=0; i<TALLA; i++)
37 vector[i] = rand();
38
39 // Se efect´ua ahora la selecci´on de pares.
40 seleccion = selecciona_pares(vector, TALLA);
41 // La variable seleccion apunta ahora a la zona de memoria con los elementos pares.
42
43 // S´ı, pero,
?
cu´antos elementos pares hay?
44 for (i=0; i<????; i++)
45 printf ("%dn", seleccion[i]);
46
47 free(seleccion);
48 seleccion = NULL;
49
50 return 0;
51 }
Tenemos un problema al usar selecciona_pares: no sabemos cu´antos valores ha seleccionado.
Podemos modificar la funci´on para que modifique el valor de un par´ametro que pasamos por
referencia:
1 int * selecciona_pares(int a[], int talla, int * numpares )
2 {
3 int i, j;
4 int * pares;
5
6 // Contamos el n´umero de elementos pares en el par´ametro numpares, pasado por referencia.
7 *numpares = 0;
8 for (i=0; i<talla; i++)
9 if (a[i] % 2 == 0)
10 (*numpares)++;
11
12 pares = malloc( *numpares * sizeof(int) );
13
14 j = 0;
15 for (i=0; i<talla; i++)
16 if (a[i] % 2 == 0)
17 pares[j++] = a[i];
218 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
18
19 return pares;
20 }
Ahora podemos resolver el problema:
pares.c pares.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <time.h>
4
5 #define TALLA 10
6
7 int * selecciona_pares(int a[], int talla, int * numpares)
8 {
9 int i, j;
10 int * pares;
11
12 // Contamos el n´umero de elementos pares en el par´ametro numpares, pasado por referencia.
13 *numpares = 0;
14 for (i=0; i<talla; i++)
15 if (a[i] % 2 == 0)
16 (*numpares)++;
17
18 pares = malloc( *numpares * sizeof(int) );
19
20 j = 0;
21 for (i=0; i<talla; i++)
22 if (a[i] % 2 == 0)
23 pares[j++] = a[i];
24
25 return pares;
26 }
27
28 int main(void)
29 {
30 int vector[TALLA], i;
31 int * seleccion, seleccionados ;
32
33 // Llenamos el vector con valores aleatorios.
34 srand(time(0));
35 for (i=0; i<TALLA; i++)
36 vector[i] = rand();
37
38 // Se efect´ua ahora la selecci´on de pares.
39 seleccion = selecciona_pares(vector, TALLA, &seleccionados );
40 // La variable seleccion apunta ahora a la zona de memoria con los elementos pares.
41 // Adem´as, la variable seleccionados contiene el n´umero de pares.
42
43 // Ahora los mostramos en pantalla.
44 for (i=0; i<seleccionados ; i++)
45 printf ("%dn", seleccion[i]);
46
47 free(seleccion);
48 seleccion = NULL;
49
50 return 0;
51 }
Por cierto, el prototipo de la funci´on, que es ´este:
int * selecciona_pares(int a[], int talla, int * seleccionados);
puede cambiarse por este otro:
int * selecciona_pares(int * a, int talla, int * seleccionados);
Introducci´on a la Programaci´on con C 219
4.1 Vectores din´amicos
Conceptualmente, es lo mismo un par´ametro declarado como int a[] que como int * a: ambos
son, en realidad, punteros a enteros3
. No obstante, es preferible utilizar la primera forma cuando
un par´ametro es un vector de enteros, ya que as´ı lo distinguimos f´acilmente de un entero pasado
por referencia. Si ves el ´ultimo prototipo, no hay nada que te permita saber si a es un vector o
un entero pasado por referencia como seleccionados. Es m´as legible, pues, la primera forma.
No puedes devolver punteros a datos locales
Como un vector de enteros y un puntero a una secuencia de enteros son, en cierto modo,
equivalentes, puede que esta funci´on te parezca correcta:
int * primeros(void)
{
int i, v[10];
for (i=0; i<10; i++)
v[i] = i + 1;
return v;
}
La funci´on devuelve, a fin de cuentas, una direcci´on de memoria en la que empieza una
secuencia de enteros. Y es verdad: eso es lo que hace. El problema radica en que la memoria
a la que apunta ¡no ((existe)) fuera de la funci´on! La memoria que ocupa v se libera tan
pronto finaliza la ejecuci´on de la funci´on. Este intento de uso de la funci´on, por ejemplo,
trata de acceder ilegalmente a memoria:
int main(void)
{
int * a;
a = primeros();
printf ("%d ", a[i]); // No existe a[i].
}
Recuerda: si devuelves un puntero, ´este no puede apuntar a datos locales.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 219 Dise˜na una funci´on que seleccione todos los n´umeros positivos de un vector de enteros.
La funci´on recibir´a el vector original y un par´ametro con su longitud y devolver´a dos datos: un
puntero al nuevo vector de enteros positivos y su longitud. El puntero se devolver´a como valor
de retorno de la funci´on, y la longitud mediante un par´ametro adicional (un entero pasado por
referencia).
· 220 Desarrolla una funci´on que seleccione todos los n´umeros de un vector de float mayores
que un valor dado. Dise˜na un programa que llame correctamente a la funci´on y muestre por
pantalla el resultado.
· 221 Escribe un programa que lea por teclado un vector de float cuyo tama˜no se solicitar´a
previamente al usuario. Una vez le´ıdos los componentes del vector, el programa copiar´a sus
valores en otro vector distinto que ordenar´a con el m´etodo de la burbuja. Recuerda liberar toda
memoria din´amica solicitada antes de finalizar el programa.
· 222 Escribe una funci´on que lea por teclado un vector de float cuyo tama˜no se solicitar´a
previamente al usuario. Escribe, adem´as, una funci´on que reciba un vector como el le´ıdo en la
funci´on anterior y devuelva una copia suya con los mismos valores, pero ordenados de menor a
mayor (usa el m´etodo de ordenaci´on de la burbuja o cualquier otro que conozcas).
Dise˜na un programa que haga uso de ambas funciones. Recuerda que debes liberar toda
memoria din´amica solicitada antes de finalizar la ejecuci´on del programa.
· 223 Escribe una funci´on que reciba un vector de enteros y devuelva otro con sus n mayores
valores, siendo n un n´umero menor o igual que la talla del vector original.
3En realidad, hay una peque˜na diferencia. La declaraci´on int a[] hace que a sea un puntero inmutable,
mientras que int * a permite modificar la direcci´on apuntada por a haciendo, por ejemplo, a++. De todos
modos, no haremos uso de esa diferencia en este texto.
220 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
· 224 Escribe una funci´on que reciba un vector de enteros y un valor n. Si n es menor o
igual que la talla del vector, la funci´on devolver´a un vector con las n primeras celdas del vector
original. En caso contrario, devolver´a un vector de n elementos con un copia del contenido del
original y con valores nulos hasta completarlo.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
No resulta muy elegante que una funci´on devuelva valores mediante return y, a la vez, me-
diante par´ametros pasados por referencia. Una posibilidad es usar ´unicamente valores pasados
por referencia:
pares 1.c pares.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <time.h>
4
5 #define TALLA 10
6
7 void selecciona_pares(int a[], int talla, int * pares[], int * numpares)
8 {
9 int i, j;
10
11 *numpares = 0;
12 for (i=0; i<talla; i++)
13 if (a[i] % 2 == 0)
14 (*numpares)++;
15
16 *pares = malloc(*numpares * sizeof(int) );
17
18 j = 0;
19 for (i=0; i<talla; i++)
20 if (a[i] % 2 == 0)
21 (*pares)[j++] = a[i];
22 }
23
24 int main(void)
25 {
26 int vector[TALLA], i;
27 int * seleccion , seleccionados;
28
29 srand(time(0));
30 for (i=0; i<TALLA; i++)
31 vector[i] = rand();
32
33 selecciona_pares(vector, TALLA, &seleccion , &seleccionados);
34
35 for (i=0; i<seleccionados; i++)
36 printf ("%dn", seleccion[i]);
37
38 free(seleccion);
39 seleccion = NULL;
40
41 return 0;
42 }
F´ıjate en la declaraci´on del par´ametro pares en la l´ınea 7: es un puntero a un vector de
enteros, o sea, un vector de enteros cuya direcci´on se suministra a la funci´on. ¿Por qu´e? Porque
a resultas de llamar a la funci´on, la direcci´on apuntada por pares ser´a una ((nueva)) direcci´on (la
que obtengamos mediante una llamada a malloc). La l´ınea 16 asigna un valor a *pares. Resulta
interesante que veas c´omo se asigna valores al vector apuntado por *pares en la l´ınea 21 (los
par´entesis alrededor de *pares son obligatorios). Finalmente, observa que seleccion se declara
en la l´ınea 27 como un puntero a entero y que se pasa la direcci´on en la que se almacena dicho
puntero en la llamada a selecciona_pares desde la l´ınea 33.
Hay una forma alternativa de indicar que pasamos la direcci´on de memoria de un puntero
de enteros. La cabecera de la funci´on selecciona_pares podr´ıa haberse definido as´ı:
Introducci´on a la Programaci´on con C 221
4.1 Vectores din´amicos
void selecciona_pares(int a[], int talla, int ** pares , int * numpares)
¿Ves c´omo usamos un doble asterisco?
Valores de retorno como aviso de errores
Es habitual que aquellas funciones C que pueden dar lugar a errores nos adviertan de ellos
mediante el valor de retorno. La funci´on malloc, por ejemplo, devuelve el valor NULL cuando
no consigue reservar la memoria solicitada y un valor diferente cuando s´ı lo consigue. La
funci´on scanf , que hemos estudiado como si no devolviese valor alguno, s´ı lo hace: devuelve
el n´umero de elementos cuyo valor ha sido efectivamente le´ıdo. Si, por ejemplo, llamamos
a scanf ("%d %d", &a, &b), la funci´on devuelve el valor 2 si todo fue bien (se ley´o el
contenido de dos variables). Si devuelve el valor 1, es porque s´olo consigui´o leer el valor de
a, y si devuelve el valor 0, no consigui´o leer ninguno de los dos. Un programa robusto debe
comprobar el valor devuelto siempre que se efect´ue una llamada a scanf ; as´ı:
1 if ( scanf ("%d %d", &a, &b) != 2)
2 printf ("Error! No consegu´ı leer los valores de a y b.n");
3 else {
4 // Situaci´on normal.
5 ...
6 }
Las rutinas que nosotros dise˜namos deber´ıan presentar un comportamiento similar. La fun-
ci´on selecciona_pares, por ejemplo, podr´ıa implementarse as´ı:
1 int selecciona_pares(int a[], int talla, int * pares[], int * numpares)
2 {
3 int i, j;
4
5 *numpares = 0;
6 for (i=0; i<talla; i++)
7 if (a[i] % 2 == 0)
8 (*numpares)++;
9 *pares = malloc(*numpares * sizeof(int) );
10 if (*pares == NULL) { // Algo fue mal: no conseguimos la memoria.
11 *numpares = 0; // Informamos de que el vector tiene capacidad 0...
12 return 0; // y devolvemos el valor 0 para advertir de que hubo un error.
13 }
14 j = 0;
15 for (i=0; i<talla; i++)
16 if (a[i] % 2 == 0)
17 (*pares)[j++] = a[i];
18 return 1; // Si llegamos aqu´ı, todo fue bien, as´ı que avisamos de ello con el valor 1.
19 }
Aqu´ı tienes un ejemplo de uso de la nueva funci´on:
1 if ( selecciona_pares(vector, TALLA, &seleccion , &seleccionados) ) {
2 // Todo va bien.
3 }
4 else {
5 // Algo fue mal.
6 }
Hay que decir, no obstante, que esta forma de aviso de errores empieza a quedar obsoleto.
Los lenguajes de programaci´on m´as modernos, como C++ o Python, suelen basar la detecci´on
(y el tratamiento) de errores en las denominadas ((excepciones)).
M´as elegante resulta definir un registro ((vector din´amico de enteros)) que almacene conjun-
tamente tanto el vector de elementos propiamente dicho como el tama˜no del vector4
:
4Aunque recomendemos este nuevo m´etodo para gestionar vectores de tama˜no variable, has de saber, cuando
menos, leer e interpretar correctamente par´ametros con tipos como int a[], int *a, int *a[] o int **a, pues
muchas veces tendr´as que utilizar bibliotecas escritas por otros programadores o leer c´odigo fuente de programas
cuyos dise˜nadores optaron por estos estilos de paso de par´ametros.
222 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
pares 2.c pares.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <time.h>
4
5 struct VectorDinamicoEnteros {
6 int * elementos; // Puntero a la zona de memoria con los elementos.
7 int talla; // N´umero de enteros almacenados en esa zona de memoria.
8 };
9
10 struct VectorDinamicoEnteros selecciona_pares(struct VectorDinamicoEnteros entrada)
11 // Recibe un vector din´amico y devuelve otro con una selecci´on de los elementos
12 // pares del primero.
13 {
14 int i, j;
15 struct VectorDinamicoEnteros pares;
16
17 pares.talla = 0;
18 for (i=0; i<entrada.talla; i++)
19 if (entrada.elementos[i] % 2 == 0)
20 pares.talla++;
21
22 pares.elementos = malloc(pares.talla * sizeof(int) );
23
24 j = 0;
25 for (i=0; i<entrada.talla; i++)
26 if (entrada.elementos[i] % 2 == 0)
27 pares.elementos[j++] = entrada.elementos[i];
28
29 return pares;
30 }
31
32 int main(void)
33 {
34 int i;
35 struct VectorDinamicoEnteros vector, seleccionados;
36
37 vector.talla = 10;
38 vector.elementos = malloc(vector.talla * sizeof(int));
39 srand(time(0));
40 for (i=0; i<vector.talla; i++)
41 vector.elementos[i] = rand();
42
43 seleccionados = selecciona_pares(vector);
44
45 for (i=0; i<seleccionados.talla; i++)
46 printf ("%dn", seleccionados.elementos[i]);
47
48 free(seleccionados.elementos);
49 seleccionados.elementos = NULL;
50 seleccionados.talla = 0;
51
52 return 0;
53 }
El ´unico problema de esta aproximaci´on es la potencial fuente de ineficiencia que supone
devolver una copia de un registro, pues podr´ıa ser de gran tama˜no. No es nuestro caso: un
struct VectorDinamicoEnteros ocupa s´olo 8 bytes. Si el tama˜no fuera un problema, podr´ıamos
usar una variable de ese tipo como par´ametro pasado por referencia. Usar´ıamos as´ı s´olo 4 bytes:
pares 3.c pares.c
1 #include <stdio.h>
2 #include <stdlib.h>
Introducci´on a la Programaci´on con C 223
4.1 Vectores din´amicos
3
4 struct VectorDinamicoEnteros {
5 int * elementos;
6 int talla;
7 };
8
9 void selecciona_pares(struct VectorDinamicoEnteros entrada,
10 struct VectorDinamicoEnteros * pares)
11 {
12 int i, j;
13
14 pares->talla = 0;
15 for (i=0; i<entrada.talla; i++)
16 if (entrada.elementos[i] % 2 == 0)
17 pares->talla++;
18
19 pares->elementos = malloc(pares->talla * sizeof(int) );
20
21 j = 0;
22 for (i=0; i<entrada.talla; i++)
23 if (entrada.elementos[i] % 2 == 0)
24 pares->elementos[j++] = entrada.elementos[i];
25 }
26
27 int main(void)
28 {
29 int i;
30 struct VectorDinamicoEnteros vector, seleccionados;
31
32 vector.talla = 10;
33 vector.elementos = malloc(vector.talla * sizeof(int));
34 for (i=0; i<vector.talla; i++)
35 vector.elementos[i] = rand();
36
37 selecciona_pares(vector, &seleccionados);
38
39 for (i=0; i<seleccionados.talla; i++)
40 printf ("%dn", seleccionados.elementos[i]);
41
42 free(seleccionados.elementos);
43 seleccionados.elementos = NULL;
44 seleccionados.talla = 0;
45
46 return 0;
47 }
Como ves, tienes muchas soluciones t´ecnicamente diferentes para realizar lo mismo. Deber´as
elegir en funci´on de la elegancia de cada soluci´on y de su eficiencia.
Listas Python
Empieza a quedar claro que Python es un lenguaje mucho m´as c´omodo que C para gestionar
vectores din´amicos, que all´ı denomin´abamos listas. No obstante, debes tener presente que
el int´erprete de Python est´a escrito en C, as´ı que cuando manejas listas Python est´as,
indirectamente, usando memoria din´amica como malloc y free.
Cuando creas una lista Python con una orden como a = [0] * 5 o a = [0, 0, 0, 0, 0],
est´as reservando espacio en memoria para 5 elementos y asign´andole a cada elemento el
valor 0. La variable a puede verse como un simple puntero a esa zona de memoria (en
realidad es algo m´as complejo).
Cuando se pierde la referencia a una lista (por ejemplo, cambiando el valor asignado
a a), Python se encarga de detectar autom´aticamente que la lista ya no es apuntada por
nadie y de llamar a free para que la memoria que hasta ahora ocupaba pase a quedar libre.
224 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
Representaci´on de pol´ıgonos con un n´umero arbitrario de v´ertices
Desarrollemos un ejemplo m´as: un programa que lea los v´ertices de un pol´ıgono y calcule su
per´ımetro. Empezaremos por crear un tipo de datos para almacenar los puntos de un pol´ıgono.
Nuestro tipo de datos se define as´ı:
struct Punto {
float x, y;
};
struct Poligono {
struct Punto * p;
int puntos;
};
F´ıjate en que un pol´ıgono presenta un n´umero de puntos inicialmente desconocido, por
lo que hemos de recurrir a memoria din´amica. Reservaremos la memoria justa para guardar
dichos puntos en el campo p (un puntero a una secuencia de puntos) y el n´umero de puntos se
almacenar´a en el campo puntos.
Aqu´ı tienes una funci´on que lee un pol´ıgono por teclado y devuelve un registro con el
resultado:
1 struct Poligono lee_poligono(void)
2 {
3 int i;
4 struct Poligono pol;
5
6 printf ("N´umero de puntos: "); scanf ("%d", &pol.puntos);
7 pol.p = malloc( pol.puntos * sizeof(struct Punto));
8 for (i=0; i<pol.puntos; i++) {
9 printf ("Punto %dn", i);
10 printf ("x: "); scanf ("%f", &pol.p[i].x);
11 printf ("y: "); scanf ("%f", &pol.p[i].y);
12 }
13 return pol;
14 }
Es interesante la forma en que solicitamos memoria para el vector de puntos:
pol.p = malloc( pol.puntos * sizeof(struct Punto));
Solicitamos memoria para pol.puntos celdas, cada una con capacidad para un dato de tipo
struct Punto (es decir, ocupando sizeof(struct Punto) bytes).
Nos vendr´a bien una funci´on que libere la memoria solicitada para almacenar un pol´ıgono,
ya que, de paso, pondremos el valor correcto en el campo puntos:
1 void libera_poligono(struct Poligono * pol)
2 {
3 free (pol->p);
4 pol->p = NULL;
5 pol->puntos = 0;
6 }
Vamos ahora a definir una funci´on que calcula el per´ımetro de un pol´ıgono:
1 float perimetro_poligono(struct Poligono pol)
2 {
3 int i;
4 float perim = 0.0;
5
6 for (i=1; i<pol.puntos; i++)
7 perim += sqrt( (pol.p[i].x - pol.p[i-1].x) * (pol.p[i].x - pol.p[i-1].x) +
8 (pol.p[i].y - pol.p[i-1].y) * (pol.p[i].y - pol.p[i-1].y) );
9 perim += sqrt( (pol.p[pol.puntos-1].x - pol.p[0].x) * (pol.p[pol.puntos-1].x - pol.p[0].x) +
10 (pol.p[pol.puntos-1].y - pol.p[0].y) * (pol.p[pol.puntos-1].y - pol.p[0].y) );
11 return perim;
12 }
Introducci´on a la Programaci´on con C 225
4.1 Vectores din´amicos
Es importante que entiendas bien expresiones como pol.p[i].x. Esa, en particular, significa: del
par´ametro pol, que es un dato de tipo struct Poligono, accede al componente i del campo p,
que es un vector de puntos; dicho componente es un dato de tipo struct Punto, pero s´olo nos
interesa acceder a su campo x (que, por cierto, es de tipo float).
Juntemos todas las piezas y a˜nadamos un sencillo programa principal que invoque a las
funciones desarrolladas:
polinomios dinamicos.c polinomios dinamicos.c
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 struct Punto {
5 float x, y;
6 };
7
8 struct Poligono {
9 struct Punto * p;
10 int puntos;
11 };
12
13 struct Poligono lee_poligono(void)
14 {
15 int i;
16 struct Poligono pol;
17
18 printf ("N´umero de puntos: "); scanf ("%d", &pol.puntos);
19 pol.p = malloc( pol.puntos * sizeof(struct Punto));
20 for (i=0; i<pol.puntos; i++) {
21 printf ("Punto %dn", i);
22 printf ("x: "); scanf ("%f", &pol.p[i].x);
23 printf ("y: "); scanf ("%f", &pol.p[i].y);
24 }
25 return pol;
26 }
27
28 void libera_poligono(struct Poligono * pol)
29 {
30 free (pol->p);
31 pol->p = NULL;
32 pol->puntos = 0;
33 }
34
35 float perimetro_poligono(struct Poligono pol)
36 {
37 int i;
38 float perim = 0.0;
39
40 for (i=1; i<pol.puntos; i++)
41 perim += sqrt( (pol.p[i].x - pol.p[i-1].x) * (pol.p[i].x - pol.p[i-1].x) +
42 (pol.p[i].y - pol.p[i-1].y) * (pol.p[i].y - pol.p[i-1].y) );
43 perim += sqrt( (pol.p[pol.puntos-1].x - pol.p[0].x) * (pol.p[pol.puntos-1].x - pol.p[0].x) +
44 (pol.p[pol.puntos-1].y - pol.p[0].y) * (pol.p[pol.puntos-1].y - pol.p[0].y) );
45 return perim;
46 }
47
48 int main(void)
49 {
50 struct Poligono un_poligono;
51 float perimetro;
52
53 un_poligono = lee_poligono();
54 perimetro = perimetro_poligono(un_poligono);
55 printf ("Per´ımetro %fn", perimetro);
226 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
56 libera_poligono(&un_poligono);
57
58 return 0;
59 }
No es el ´unico modo en que podr´ıamos haber escrito el programa. Te presentamos ahora
una implementaci´on con bastantes diferencias en el modo de paso de par´ametros:
polinomios dinamicos 1.c polinomios dinamicos.c
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 struct Punto {
5 float x, y;
6 };
7
8 struct Poligono {
9 struct Punto * p;
10 int puntos;
11 };
12
13 void lee_poligono(struct Poligono * pol)
14 {
15 int i;
16
17 printf ("N´umero de puntos: "); scanf ("%d", &pol->puntos);
18 pol->p = malloc( pol->puntos * sizeof(struct Punto));
19 for (i=0; i<pol->puntos; i++) {
20 printf ("Punto %dn", i);
21 printf ("x: "); scanf ("%f", &pol->p[i].x);
22 printf ("y: "); scanf ("%f", &pol->p[i].y);
23 }
24 }
25
26 void libera_poligono(struct Poligono * pol)
27 {
28 free (pol->p);
29 pol->p = NULL;
30 pol->puntos = 0;
31 }
32
33 float perimetro_poligono(const struct Poligono * pol)
34 {
35 int i;
36 float perim = 0.0;
37
38 for (i=1; i<pol->puntos; i++)
39 perim += sqrt( (pol->p[i].x - pol->p[i-1].x) * (pol->p[i].x - pol->p[i-1].x) +
40 (pol->p[i].y - pol->p[i-1].y) * (pol->p[i].y - pol->p[i-1].y) );
41 perim +=
42 sqrt((pol->p[pol->puntos-1].x - pol->p[0].x) * (pol->p[pol->puntos-1].x - pol->p[0].x) +
43 (pol->p[pol->puntos-1].y - pol->p[0].y) * (pol->p[pol->puntos-1].y - pol->p[0].y) );
44 return perim;
45 }
46
47 int main(void)
48 {
49 struct Poligono un_poligono;
50 float perimetro;
51
52 lee_poligono(&un_poligono);
53 perimetro = perimetro_poligono(&un_poligono);
54 printf ("Per´ımetro %fn", perimetro);
55 libera_poligono(&un_poligono);
Introducci´on a la Programaci´on con C 227
4.1 Vectores din´amicos
56
57 return 0;
58 }
En esta versi´on hemos optado, siempre que ha sido posible, por el paso de par´ametros por
referencia, es decir, por pasar la direcci´on de la variable en lugar de una copia de su contenido.
Hay una raz´on para hacerlo: la eficiencia. Cada dato de tipo struct Poligono esta formado por
un puntero (4 bytes) y un entero (4 bytes), as´ı que ocupa 8 bytes. Si pasamos o devolvemos una
copia de un struct Poligono, estamos copiando 8 bytes. Si, por contra, pasamos su direcci´on de
memoria, s´olo hay que pasar 4 bytes. En este caso particular no hay una ganancia extraordinaria,
pero en otras aplicaciones manejar´as structs tan grandes que el paso de la direcci´on compensar´a
la ligera molestia de la notaci´on de acceso a campos con el operador ->.
Puede que te extra˜ne el t´ermino const calificando el par´ametro de perimetro_poligono. Su
uso es opcional y sirve para indicar que, aunque es posible modificar la informaci´on apuntada por
pol, no lo haremos. En realidad suministramos el puntero por cuesti´on de eficiencia, no porque
deseemos modificar el contenido. Con esta indicaci´on conseguimos dos efectos: si intent´asemos
modificar accidentalmente el contenido, el compilador nos advertir´ıa del error; y, si fuera posible,
el compilador efectuar´ıa optimizaciones que no podr´ıa aplicar si la informaci´on apuntada por
pol pudiera modificarse en la funci´on.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 225 ¿Funciona esta otra implementaci´on de perimetro_poligono?
1 float perimetro_poligono(struct Poligono pol)
2 {
3 int i;
4 float perim = 0.0;
5
6 for (i=1; i<pol.puntos +1; i++)
7 perim +=
8 sqrt((pol.p[i%pol.puntos ].x - pol.p[i-1].x) * (pol.p[i%pol.puntos ].x - pol.p[i-1].x)+
9 (pol.p[i%pol.puntos ].y - pol.p[i-1].y) * (pol.p[i%pol.puntos ].y - pol.p[i-1].y));
10 return perim;
11 }
· 226 Dise˜na una funci´on que cree un pol´ıgono regular de n lados inscrito en una circunfe-
rencia de radio r. Esta figura muestra un pent´agono inscrito en una circunferencia de radio r y
las coordenadas de cada uno de sus v´ertices:
(r cos(0), r sin(0))
(r cos(2π/5), r sin(2π/5))
(r cos(2π · 2/5), r sin(2π · 2/5))
(r cos(2π · 3/5), r sin(2π · 3/5))
(r cos(2π · 4/5), r sin(2π · 4/5))
r
Utiliza la funci´on para crear pol´ıgonos regulares de talla 3, 4, 5, 6, . . . inscritos en una
circunferencia de radio 1. Calcula a continuaci´on el per´ımetro de los sucesivos pol´ıgonos y
comprueba si dicho valor se aproxima a 2π.
· 227 Dise˜na un programa que permita manipular polinomios de cualquier grado. Un poli-
nomio se representar´a con el siguiente tipo de registro:
1 struct Polinomio {
2 float * p;
3 int grado;
4 };
Como puedes ver, el campo p es un puntero a float, o sea, un vector din´amico de float. Dise˜na
y utiliza funciones que hagan lo siguiente:
228 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
Leer un polinomio por teclado. Se pedir´a el grado del polinomio y, tras reservar memoria
suficiente para sus coeficientes, se pedir´a tambi´en el valor de cada uno de ellos.
Evaluar un polinomio p(x) para un valor dado de x.
Sumar dos polinomios. Ten en cuenta que cada uno de ellos puede ser de diferente grado
y el resultado tendr´a, en principio, grado igual que el mayor grado de los operandos. (Hay
excepciones; piensa cu´ales.)
Multiplicar dos polinomios.
· 228 Dise˜na un programa que solicite la talla de una serie de valores enteros y dichos valores.
El programa ordenar´a a continuaci´on los valores mediante el procedimiento mergesort. (Ten en
cuenta que el vector auxiliar que necesita merge debe tener capacidad para el mismo n´umero
de elementos que el vector original.)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Reserva con inicializaci´on autom´atica
La funci´on calloc es similar a malloc, pero presenta un prototipo diferente y hace algo m´as
que reservar memoria: la inicializa a cero. He aqu´ı un prototipo (similar al) de calloc:
void * calloc(int nmemb, int size);
Con calloc, puedes pedir memoria para un vector de talla enteros as´ı:
a = calloc(talla, sizeof(int));
El primer par´ametro es el n´umero de elementos y el segundo, el n´umero de bytes que ocupa
cada elemento. No hay que multiplicar una cantidad por otra, como hac´ıamos con malloc.
Todos los enteros del vector se inicializan a cero. Es como si ejecut´asemos este fragmento
de c´odigo:
a = malloc( talla * sizeof(int) );
for (i = 0; i < talla; i++) a[i] = 0;
¿Por qu´e no usar siempre calloc, si parece mejor que malloc? Por eficiencia. En ocasiones
no desear´as que se pierda tiempo de ejecuci´on inicializando la memoria a cero, ya que t´u
mismo querr´as inicializarla a otros valores inmediatamente. Recuerda que garantizar la mayor
eficiencia de los programas es uno de los objetivos del lenguaje de programaci´on C.
4.1.3. Cadenas din´amicas
Las cadenas son un caso particular de vector. Podemos usar cadenas de cualquier longitud
gracias a la gesti´on de memoria din´amica. Este programa, por ejemplo, lee dos cadenas y
construye una nueva que resulta de concatenar a ´estas.
cadenas dinamicas.c cadenas dinamicas.c
1 #include <stdio.h>
2 #include <string.h>
3 #include <stdlib.h>
4
5 #define CAPACIDAD 80
6
7 int main(void)
8 {
9 char cadena1[CAPACIDAD+1], cadena2[CAPACIDAD+1];
10 char * cadena3;
11
12 printf ("Dame un texto: "); gets(cadena1);
13 printf ("Dame otro texto: "); gets(cadena2);
14
15 cadena3 = malloc( (strlen(cadena1) + strlen(cadena2) + 1) * sizeof(char) );
Introducci´on a la Programaci´on con C 229
4.2 Matrices din´amicas
16
17 strcpy(cadena3, cadena1);
18 strcat(cadena3, cadena2);
19
20 printf ("Resultado de concatenar ambos: %sn", cadena3);
21
22 free(cadena3);
23 cadena3 = NULL;
24
25 return 0;
26 }
Como las dos primeras cadenas se leen con gets, hemos de definirlas como cadenas est´aticas.
La tercera cadena reserva exactamente la misma cantidad de memoria que ocupa.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 229 Dise˜na una funci´on que lea una cadena y construya otra con una copia invertida de
la primera. La segunda cadena reservar´a s´olo la memoria que necesite.
· 230 Dise˜na una funci´on que lea una cadena y construya otra que contenga un ejemplar de
cada car´acter de la primera. Por ejemplo, si la primera cadena es "este ejemplo", la segunda
ser´a "est jmplo". Ten en cuenta que la segunda cadena debe ocupar la menor cantidad de
memoria posible.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Sobre la mutabilidad de las cadenas
Es posible inicializar un puntero a cadena de modo que apunte a un literal de cadena:
char * p = "cadena";
Pero, ¡ojo!, la cadena apuntada por p es, en ese caso, inmutable: si intentas asignar un
char a p[i], el programa puede abortar su ejecuci´on. ¿Por qu´e? Porque los literales de
cadena ((residen)) en una zona de memoria especial (la denominada ((zona de texto))) que
est´a protegida contra escritura. Y hay una raz´on para ello: en esa zona reside, tambi´en,
el c´odigo de m´aquina correspondiente al programa. Que un programa modifique su propio
c´odigo de m´aquina es una p´esima pr´actica (que era relativamente frecuente en los tiempos
en que predominaba la programaci´on en ensamblador), hasta el punto de que su zona de
memoria se marca como de s´olo lectura.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 231 Implementa una funci´on que reciba una cadena y devuelva una copia invertida. (Ten
en cuenta que la talla de la cadena puede conocerse con strlen, as´ı que no es necesario que
suministres la talla expl´ıcitamente ni que devuelvas la talla de la memoria solicitada con un
par´ametro pasado por referencia.)
Escribe un programa que solicite varias palabras a un usuario y muestre el resultado de
invertir cada una de ellas.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2. Matrices din´amicas
Podemos extender la idea de los vectores din´amicos a matrices din´amicas. Pero el asunto se com-
plica notablemente: no podemos gestionar la matriz como una sucesi´on de elementos contiguos,
sino como un ((vector din´amico de vectores din´amicos)).
4.2.1. Gesti´on de memoria para matrices din´amicas
Analiza detenidamente este programa:
230 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
matriz dinamica.c matriz dinamica.c
1 #define <stdio.h>
2 #define <stdlib.h>
3
4 int main(void)
5 {
6 float ** m = NULL;
7 int filas, columnas;
8
9 printf ("Filas: "); scanf ("%d", &filas);
10 printf ("Columnas: "); scanf ("%d", &columnas);
11
12 /* reserva de memoria */
13 m = malloc(filas * sizeof(float *));
14 for (i=0; i<filas; i++)
15 m[i] = malloc(columnas * sizeof(float));
16
17 /* trabajo con m[i][j] */
18 ...
19
20 /* liberaci´on de memoria */
21 for (i=0; i<filas; i++)
22 free(m[i]);
23 free(m);
24 m = NULL;
25
26 return 0;
27 }
Analicemos poco a poco el programa.
Declaraci´on del tipo
Empecemos por la declaraci´on de la matriz (l´ınea 6). Es un puntero un poco extra˜no: se declara
como float ** m. Dos asteriscos, no uno. Eso es porque se trata de un puntero a un puntero de
enteros o, equivalentemente, un vector din´amico de vectores din´amicos de enteros.
Reserva de memoria
Sigamos. Las l´ıneas 9 y 10 solicitan al usuario los valores de filas y columnas. En la l´ınea 13
encontramos una petici´on de memoria. Se solicita espacio para un n´umero filas de punteros
a float. Supongamos que filas vale 4. Tras esa petici´on, tenemos la siguiente asignaci´on de
memoria para m:
m
0
1
2
3
El vector m es un vector din´amico cuyos elementos son punteros (del tipo float *). De
momento, esos punteros no apuntan a ninguna zona de memoria reservada. De ello se encarga
la l´ınea 15. Dicha l´ınea est´a en un bucle, as´ı que se ejecuta para m[0], m[1], m[2], . . . El
efecto es proporcionar un bloque de memoria para cada celda de m. He aqu´ı el efecto final:
Introducci´on a la Programaci´on con C 231
4.2 Matrices din´amicas
m
0
0 1 2 3 4
1
0 1 2 3 4
2
0 1 2 3 4
3
0 1 2 3 4
Acceso a filas y elementos
Bien. ¿Y c´omo se usa m ahora? ¡Como cualquier matriz! Pensemos en qu´e ocurre cuando
accedemos a m[1][2]. Analicemos m[1][2] de izquierda a derecha. Primero tenemos a m,
que es un puntero (tipo float **), o sea, un vector din´amico a elementos del tipo float *. El
elemento m[1] es el segundo componente de m. ¿Y de qu´e tipo es? De tipo float *, un nuevo
puntero o vector din´amico, pero a valores de tipo float. Si es un vector din´amico, lo podemos
indexar, as´ı que es v´alido escribir m[1][2]. ¿Y de qu´e tipo es eso? De tipo float. F´ıjate:
m es de tipo float **;
m[1] es de tipo float *;
m[1][2] es de tipo float.
Con cada indexaci´on, ((desaparece)) un asterisco del tipo de datos.
Liberaci´on de memoria: un free para cada malloc
Sigamos con el programa. Nos resta la liberaci´on de memoria. Observa que hay una llamada a
free por cada llamada a malloc realizada con anterioridad (l´ıneas 20–24). Hemos de liberar cada
uno de los bloques reservados y hemos de empezar a hacerlo por los de ((segundo nivel)), es decir,
por los de la forma m[i]. Si empez´asemos liberando m, cometer´ıamos un grave error: si libera-
mos m antes que todos los m[i], perderemos el puntero que los referencia y, en consecuencia,
¡no podremos liberarlos!
...
free(m);
m = NULL;
/* liberaci´on de memoria incorrecta:
?
qu´e es m[i] ahora que m vale NULL? */
for (i=0; i<filas; i++)
free(m[i]);
}
Matrices din´amicas y funciones
El paso de matrices din´amicas a funciones tiene varias formas idiom´aticas que conviene que
conozcas. Imagina una funci´on que recibe una matriz de enteros para mostrar su contenido por
pantalla. En principio, la cabecera de la funci´on presentar´ıa este aspecto:
void muestra_matriz(int ** m)
El par´ametro indica que es de tipo ((puntero a punteros a enteros)). Una forma alternativa de
decir lo mismo es ´esta:
void muestra_matriz(int * m[])
Se lee m´as bien como ((vector de punteros a entero)). Pero ambas expresiones son sin´onimas de
((vector de vectores a entero)). Uno se siente tentado de utilizar esta otra cabecera:
void muestra_matriz(int m[][]) //
!
Mal!
Pero no funciona. Es incorrecta. C entiende que queremos pasar una matriz est´atica y que
hemos omitido el n´umero de columnas.
Sigamos con la funci´on:
232 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
M´as eficiencia, menos reservas de memoria
Te hemos ense˜nado una forma ((est´andar)) de pedir memoria para matrices din´amicas. No
es la ´unica. Es m´as, no es la m´as utilizada en la pr´actica. ¿Por qu´e? Porque obliga a realizar
tantas llamadas a malloc (y despu´es a free) como filas tiene la matriz m´as uno. Las llamadas
a malloc pueden resultar ineficientes cuando su n´umero es grande. Es posible reservar la
memoria de una matriz din´amica con s´olo dos llamadas a malloc.
1 #include <stdlib.h>
2
3 int main(void)
4 {
5 int ** m;
6 int filas, columnas;
7
8 filas = ...;
9 columnas = ...;
10
11 // Reserva de memoria.
12 m = malloc(filas * sizeof(int *));
13 m[0] = malloc(filas * columnas * sizeof(int));
14 for (i=1; i<filas; i++) m[i] = m[i-1] + columnas;
15
16 ...
17 // Liberaci´on de memoria.
18 free(m[0]);
19 free(m);
20
21 return 0;
22 }
La clave est´a en la sentencia m[i] = m[i-1] + columnas: el contenido de m[i] pasa a ser
la direcci´on de memoria columnas celdas m´as a la derecha de la direcci´on m[i-1]. He aqu´ı
una representaci´on gr´afica de una matriz de 5 × 4:
m 0
1
2
3
4
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
1 void muestra_matriz(int ** m )
2 {
3 int i,j;
4
5 for (i=0; i<???; i++) {
6 for (j=0; j<???; j++)
7 printf ("%d ", m[i][j]);
8 printf ("n");
9 }
10 }
Observa que necesitamos suministrar el n´umero de filas y columnas expl´ıcitamente para saber
qu´e rango de valores deben tomar i y j:
1 void muestra_matriz(int ** m, int filas, int columnas)
2 {
3 int i,j;
4
5 for (i=0; i<filas; i++) {
Introducci´on a la Programaci´on con C 233
4.2 Matrices din´amicas
6 for (j=0; j<columnas; j++)
7 printf ("%d ", m[i][j]);
8 printf ("n");
9 }
10 }
Supongamos ahora que nos piden una funci´on que efect´ue la liberaci´on de la memoria de
una matriz:
1 void libera_matriz(int ** m, int filas, int columnas)
2 {
3 int i;
4
5 for (i=0; i<filas; i++)
6 free(m[i]);
7 free(m);
8 }
Ahora resulta innecesario el paso del n´umero de columnas, pues no se usa en la funci´on:
1 void libera_matriz(int ** m, int filas)
2 {
3 int i;
4
5 for (i=0; i<filas; i++)
6 free(m[i]);
7 free(m);
8 }
Falta un detalle que har´ıa mejor a esta funci´on: la asignaci´on del valor NULL a m al final de
todo. Para ello tenemos que pasar una referencia a la matriz, y no la propia matriz:
1 void libera_matriz(int *** m, int filas)
2 {
3 int i;
4
5 for (i=0; i<filas; i++)
6 free( (*m)[i]);
7 free(*m);
8 *m = NULL;
9 }
¡Qu´e horror! ¡Tres asteriscos en la declaraci´on del par´ametro m! C no es, precisamente, el colmo
de la elegancia.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 232 Dise˜na una funci´on que reciba un n´umero de filas y un n´umero de columnas y devuelva
una matriz din´amica de enteros con filas×columnas elementos.
· 233 Dise˜na un procedimiento que reciba un puntero a una matriz din´amica (sin memo-
ria asignada), un n´umero de filas y un n´umero de columnas y devuelva, mediante el primer
par´ametro, una matriz din´amica de enteros con filas×columnas elementos.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
La gesti´on de matrices din´amicas considerando por separado sus tres variables (puntero a
memoria, n´umero de filas y n´umero de columnas) resulta poco elegante y da lugar a funciones
con par´ametros de dif´ıcil lectura. En el siguiente apartado aprender´as a usar matrices din´amicas
que agrupan sus tres datos en un tipo registro definido por el usuario.
4.2.2. Definici´on de un tipo ((matriz din´amica)) y de funciones para su ges-
ti´on
Presentaremos ahora un ejemplo de aplicaci´on de lo aprendido: un programa que multiplica dos
matrices de tallas arbitrarias. Empezaremos por definir un nuevo tipo de datos para nuestras
matrices. El nuevo tipo ser´a un struct que contendr´a una matriz din´amica de float y el n´umero
de filas y columnas.
234 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
1 struct Matriz {
2 float ** m;
3 int filas, columnas;
4 };
Dise˜nemos ahora una funci´on que ((cree)) una matriz dado el n´umero de filas y el n´umero de
columnas:
1 struct Matriz crea_matriz (int filas, int columnas)
2 {
3 struct Matriz mat;
4 int i;
5
6 if (filas <= 0 || columnas <=0) {
7 mat.filas = mat.columnas = 0;
8 mat.m = NULL;
9 return mat;
10 }
11
12 mat.filas = filas;
13 mat.columnas = columnas;
14 mat.m = malloc ( filas * sizeof(float *) );
15 for (i=0; i<filas; i++)
16 mat.m[i] = malloc ( columnas * sizeof(float) );
17 return mat;
18 }
Hemos tenido la precauci´on de no pedir memoria si el n´umero de filas o columnas no son
v´alidos. Para crear una matriz de, por ejemplo, 3 × 4, llamaremos a la funci´on as´ı:
1 struct Matriz matriz;
2 ...
3 matriz = crea_matriz(3, 4);
Hay una implementaci´on alternativa de crea_matriz:
1 void crea_matriz (int filas, int columnas, struct Matriz * mat)
2 {
3 int i;
4
5 if (filas <= 0 || columnas <=0) {
6 mat->filas = mat->columnas = 0;
7 mat->m = NULL;
8 }
9 else {
10 mat->filas = filas;
11 mat->columnas = columnas;
12 mat->m = malloc ( filas * sizeof(float *) );
13 for (i=0; i<filas; i++)
14 mat->m[i] = malloc ( columnas * sizeof(float) );
15 }
16 }
En este caso, la funci´on (procedimiento) se llamar´ıa as´ı:
1 struct Matriz matriz;
2 ...
3 crea_matriz(3, 4, &matriz);
Tambi´en nos vendr´a bien disponer de un procedimiento para liberar la memoria de una
matriz:
1 void libera_matriz (struct Matriz * mat)
2 {
3 int i;
4
Introducci´on a la Programaci´on con C 235
4.2 Matrices din´amicas
5 if (mat->m != NULL) {
6 for (i=0; i<mat->filas; i++)
7 free(mat->m[i]);
8 free(mat->m);
9 }
10
11 mat->m = NULL;
12 mat->filas = 0;
13 mat->columnas = 0;
14 }
Para liberar la memoria de una matriz din´amica m, efectuaremos una llamada como ´esta:
1 libera_matriz(&m);
Como hemos de leer dos matrices por teclado, dise˜nemos ahora una funci´on capaz de leer
una matriz por teclado:
1 struct Matriz lee_matriz (void)
2 {
3 int i, j, filas, columnas;
4 struct Matriz mat;
5
6 printf ("Filas: "); scanf ("%d", &filas);
7 printf ("Columnas: "); scanf ("%d", &columnas);
8
9 mat = crea_matriz(filas, columnas);
10
11 for (i=0; i<filas; i++)
12 for (j=0; j<columnas; j++) {
13 printf ("Elemento [%d][%d]: ", i, j); scanf ("%f", &mat.m[i][j]);
14 }
15 return mat;
16 }
Observa que hemos llamado a crea_matriz tan pronto hemos sabido cu´al era el n´umero de
filas y columnas de la matriz.
Y ahora, implementemos un procedimiento que muestre por pantalla una matriz:
1 void muestra_matriz (struct Matriz mat)
2 {
3 int i, j;
4
5 for (i=0; i<mat.filas; i++) {
6 for (j=0; j<mat.columnas; j++)
7 printf ("%f ", mat.m[i][j]);
8 printf ("n");
9 }
10 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 234 En muestra_matriz hemos pasado la matriz mat por valor. ¿Cu´antos bytes se copiar´an
en pila con cada llamada?
· 235 Dise˜na una nueva versi´on de muestra_matriz en la que mat se pase por referencia.
¿Cu´antos bytes se copiar´an en pila con cada llamada?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Podemos proceder ya mismo a implementar una funci´on que multiplique dos matrices:
1 struct Matriz multiplica_matrices (struct Matriz a, struct Matriz b)
2 {
3 int i, j, k;
4 struct Matriz c;
5
236 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
6 if (a.columnas != b.filas) { /* No se pueden multiplicar */
7 c.filas = c.columnas = 0;
8 c.m = NULL;
9 return c;
10 }
11 c = crea_matriz(a.filas, b.columnas);
12 for (i=0; i<c.filas; i++)
13 for (j=0; j<c.columnas; j++) {
14 c.m[i][j] = 0.0;
15 for (k=0; k<a.columnas; k++)
16 c.m[i][j] += a.m[i][k] * b.m[k][j];
17 }
18 return c;
19 }
No todo par de matrices puede multiplicarse entre s´ı. El n´umero de columnas de la primera
ha de ser igual al n´umero de filas de la segunda. Por eso devolvemos una matriz vac´ıa (de 0×0)
cuando a.columnas es distinto de b.filas.
Ya podemos construir el programa principal:
1 #include <stdio.h>
2
3 ...definici´on de funciones...
4
5 int main(void)
6 {
7 struct Matriz a, b, c;
8
9 a = lee_matriz();
10 b = lee_matriz();
11 c = multiplica_matrices(a, b);
12 if (c.m == NULL)
13 printf ("Las matrices no son multiplicablesn");
14 else {
15 printf ("Resultado del producto:n");
16 muestra_matriz(c);
17 }
18 libera_matriz(&a);
19 libera_matriz(&b);
20 libera_matriz(&c);
21
22 return 0;
23 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 236 Dise˜na una funci´on que sume dos matrices.
· 237 Pasar estructuras por valor puede ser ineficiente, pues se debe obtener una copia en
pila de la estructura completa (en el caso de las matrices, cada variable de tipo struct Matriz
ocupa 12 bytes —un puntero y dos enteros—, cuando una referencia supone la copia de s´olo 4
bytes). Modifica la funci´on que multiplica dos matrices para que sus dos par´ametros se pasen
por referencia.
· 238 Dise˜na una funci´on que encuentre, si lo hay, un punto de silla en una matriz. Un punto
de silla es un elemento de la matriz que es o bien el m´aximo de su fila y el m´ınimo de su columna
a la vez, o bien el m´ınimo de su fila y el m´aximo de su columna a la vez. La funci´on devolver´a
cierto o falso dependiendo de si hay alg´un punto de silla. Si lo hay, el valor del primer punto de
silla encontrado se devolver´a como valor de un par´ametro pasado por referencia.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Introducci´on a la Programaci´on con C 237
4.3 M´as all´a de las matrices din´amicas
4.3. M´as all´a de las matrices din´amicas
4.3.1. Vectores de vectores de tallas arbitrarias
Hemos aprendido a definir matrices din´amicas con un vector din´amico de vectores din´amicos. El
primero contiene punteros que apuntan a cada columna. Una caracter´ıstica de las matrices es que
todas las filas tienen el mismo n´umero de elementos (el n´umero de columnas). Hay estructuras
similares a las matrices pero que no imponen esa restricci´on. Pensemos, por ejemplo, en una
lista de palabras. Una forma de almacenarla en memoria es la que se muestra en este gr´afico:
listapal
3
2
1
0
t a c o 0
m a n o 0
d a d i v o s o 0
a n u a l 0
¿Ves? Es parecido a una matriz, pero no exactamente una matriz: cada palabra ocupa
tanta memoria como necesita, pero no m´as. Este programa solicita al usuario 4 palabras y las
almacena en una estructura como la dibujada:
cuatro palabras.c cuatro palabras.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4
5 #define PALS 4
6 #define MAXLON 80
7
8 int main(void)
9 {
10 char ** listapal;
11 char linea[MAXLON+1];
12 int i;
13
14 /* Pedir memoria y leer datos */
15 listapal = malloc(PALS * sizeof(char *));
16 for (i=0; i<PALS; i++) {
17 printf ("Teclea una palabra: ");
18 gets(linea);
19 listapal[i] = malloc( (strlen(linea)+1) * sizeof(char) );
20 strcpy(listapal[i], linea);
21 }
22
23 /* Mostrar el contenido de la lista */
24 for (i=0; i<PALS; i++)
25 printf ("Palabra %i: %sn", i, listapal[i]);
26
27 /* Liberar memoria */
28 for (i=0; i<PALS; i++)
29 free(listapal[i]);
30 free(listapal);
31
32 return 0;
33 }
Este otro programa s´olo usa memoria din´amica para las palabras, pero no para el vector de
palabras:
cuatro palabras 1.c cuatro palabras.c
1 #include <stdio.h>
2 #include <stdlib.h>
238 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
3 #include <string.h>
4
5 #define PALS 4
6 #define MAXLON 80
7
8 int main(void)
9 {
10 char * listapal[PALS];
11 char linea[MAXLON+1];
12 int i;
13
14 /* Pedir memoria y leer datos */
15 for (i=0; i<PALS; i++) {
16 printf ("Teclea una palabra: ");
17 gets(linea);
18 listapal[i] = malloc( (strlen(linea)+1) * sizeof(char) );
19 strcpy(listapal[i], linea);
20 }
21
22 /* Mostrar el contenido de la lista */
23 for (i=0; i<PALS; i++)
24 printf ("Palabra %i: %sn", i, listapal[i]);
25
26 /* Liberar memoria */
27 for (i=0; i<PALS; i++)
28 free(listapal[i]);
29
30 return 0;
31 }
F´ıjate en c´omo hemos definido listapal: como un vector est´atico de 4 punteros a caracteres
(char * listapal[PALS]).
Vamos a ilustrar el uso de este tipo de estructuras de datos con la escritura de una funci´on
que reciba una cadena y devuelva un vector de palabras, es decir, vamos a implementar la
funcionalidad que ofrece Python con el m´etodo split. Empecemos por considerar la cabecera
de la funci´on, a la que llamaremos extrae_palabras. Est´a claro que uno de los par´ametros de
entrada es una cadena, o sea, un vector de caracteres:
??? extrae_palabras(char frase[], ???)
No hace falta suministrar la longitud de la cadena, pues ´esta se puede calcular con la funci´on
strlen. ¿C´omo representamos la informaci´on de salida? Una posibilidad es devolver un vector
de cadenas:
char ** extrae_palabras(char frase[], ???)
O sea, devolvemos un puntero (*) a una serie de datos de tipo char *, o sea, cadenas. Pero a´un
falta algo: hemos de indicar expl´ıcitamente cu´antas palabras hemos encontrado:
char ** extrae_palabras(char frase[], int * numpals)
Hemos recurrido a un par´ametro adicional para devolver el segundo valor. Dicho par´ametro es
la direcci´on de un entero, pues vamos a modificar su valor. Ya podemos codificar el cuerpo de
la funci´on. Empezaremos por contar las palabras, que ser´an series de caracteres separadas por
blancos (no entraremos en mayores complicaciones acerca de qu´e es una palabra).
1 char ** extrae_palabras(char frase[], int * numpals)
2 {
3 int i, lonfrase;
4
5 lonfrase = strlen(frase);
6 *numpals = 1;
7 for (i=0; i<lonfrase-1; i++)
8 if (frase[i] == ’ ’ && frase[i+1] != ’ ’) (*numpals)++;
9 if (frase[0] == ’ ’) (*numpals)--;
10
11 ...
12 }
Introducci´on a la Programaci´on con C 239
4.3 M´as all´a de las matrices din´amicas
Acceso a argumentos de la l´ınea de comandos
Los programas que dise˜namos en el curso suponen que main no tiene par´ametros. No
siempre es as´ı.
La funci´on main puede recibir como argumentos las opciones que se indican en la l´ınea
de comandos cuando ejecutas el programa desde la l´ınea de ´ordenes Unix. El siguiente
programa muestra por pantalla un saludo personalizado y debe llamarse as´ı:
saluda -n nombre
Aqu´ı tienes el c´odigo fuente:
saluda.c
1 #include <stdio.h>
2 #include <string.h>
3
4 main (int argc, char * argv[])
5 {
6 if (argc != 3)
7 printf ("Error: necesito que indiques el nombre con -nn");
8 else
9 if (strcmp(argv[1], "-n") != 0)
10 printf ("Error: s´olo entiendo la opci´on -nn");
11 else
12 printf ("Hola, %s.", argv[2]);
13 }
El argumento argc indica cu´antas ((palabras)) se han usado en la l´ınea de ´ordenes. El
argumento argv es un vector de char *, es decir, un vector de cadenas (una cadena es un
vector de caracteres). El elemento argv[0] contiene el nombre del programa (en nuestro
caso, "saluda") que es la primera ((palabra)), argv[1] el de la segunda (que esperamos que
sea "-n") y argv[2] la tercera (el nombre de la persona a la que saludamos).
La estructura argv, tras la invocaci´on saluda -n nombre, es:
argv
2
1
0
n o m b r e 0
- n 0
s a l u d a 0
Ya podemos reservar memoria para el vector de cadenas, pero a´un no para cada una de ellas:
1 char ** extrae_palabras(char frase[], int * numpals)
2 {
3 int i, lonfrase;
4 char **palabras;
5
6 lonfrase = strlen(frase);
7 *numpals = 1;
8 for (i=0; i<lonfrase-1; i++)
9 if (frase[i] == ’ ’ && frase[i+1] != ’ ’) (*numpals)++;
10 if (frase[0] == ’ ’) (*numpals)--;
11
12 palabras = malloc(*numpals * sizeof(char *));
13
14 ...
15 }
Ahora pasamos a reservar memoria para cada una de las palabras y, tan pronto hagamos cada
reserva, ((escribirla)) en su porci´on de memoria:
1 char ** extrae_palabras(char frase[], int * numpals)
240 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
2 {
3 int i, j, inicio_pal, longitud_pal, palabra_actual, lonfrase;
4 char **palabras;
5
6 lonfrase = strlen(frase);
7 *numpals = 1;
8 for (i=0; i<lonfrase-1; i++)
9 if (frase[i] == ’ ’ && frase[i+1] != ’ ’)
10 (*numpals)++;
11 if (frase[0] == ’ ’)
12 (*numpals)--;
13
14 palabras = malloc(*numpals * sizeof(char *));
15
16 palabra_actual = 0;
17 i = 0;
18 if (frase[0] == ’ ’)
19 while (frase[++i] == ’ ’ && i < lonfrase); // Saltamos blancos iniciales.
20
21 while (i<lonfrase) {
22 inicio_pal = i;
23 while (frase[++i] != ’ ’ && i < lonfrase); // Recorremos la palabra.
24
25 longitud_pal = i - inicio_pal; // Calculamos n´umero de caracteres en la palabra actual.
26
27 palabras[palabra_actual] = malloc((longitud_pal+1)*sizeof(char)); // Reservamos memoria.
28
29 // Y copiamos la palabra de frase al vector de palabras.
30 for (j=inicio_pal; j<i; j++)
31 palabras[palabra_actual][j-inicio_pal] = frase[j];
32 palabras[palabra_actual][j-inicio_pal] = ’0’;
33
34 while (frase[++i] == ’ ’ && i < lonfrase); // Saltamos blancos entre palabras.
35
36 palabra_actual++; // Y pasamos a la siguiente.
37 }
38
39 return palabras;
40 }
¡Buf! Complicado, ¿verdad? Veamos c´omo se puede usar la funci´on desde el programa principal:
palabras.c E palabras.c E
1 #include <stdio.h>
2 #include <stdlib.h>
3
.
.
.
43 }
44
45 int main(void)
46 {
47 char linea[MAXLON+1];
48 int numero_palabras, i;
49 char ** las_palabras;
50
51 printf ("Escribe una frase: ");
52 gets(linea);
53 las_palabras = extrae_palabras(linea, &numero_palabras);
54 for (i=0; i<numero_palabras; i++)
55 printf ("%sn", las_palabras[i]);
56
57 return 0;
58 }
Introducci´on a la Programaci´on con C 241
4.3 M´as all´a de las matrices din´amicas
¿Ya? A´un no. Aunque este programa compila correctamente y se ejecuta sin problemas, hemos
de considerarlo incorrecto: hemos solicitado memoria con malloc y no la hemos liberado con
free.
palabras 1.c palabras.c
1 #include <stdio.h>
2 #include <stdlib.h>
3
.
.
.
43 }
44
45 int main(void)
46 {
47 char linea[MAXLON+1];
48 int numero_palabras, i;
49 char ** las_palabras;
50
51 printf ("Escribe una frase: ");
52 gets(linea);
53 las_palabras = extrae_palabras(linea, &numero_palabras);
54 for (i=0; i<numero_palabras; i++)
55 printf ("%sn", las_palabras[i]);
56
57 for (i=0; i<numero_palabras; i++)
58 free(las_palabras[i]);
59 free(las_palabras);
60 las_palabras = NULL;
61
62 return 0;
63 }
Ahora s´ı.
4.3.2. Arreglos n-dimensionales
Hemos considerado la creaci´on de estructuras bidimensionales (matrices o vectores de vectores),
pero nada impide definir estructuras con m´as dimensiones. Este sencillo programa pretende
ilustrar la idea creando una estructura din´amica con 3 × 3 × 3 elementos, inicializarla, mostrar
su contenido y liberar la memoria ocupada:
tridimensional.c tridimensional.c
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int main(void)
5 {
6 int *** a; // Tres asteriscos: vector de vectores de vectores de enteros.
7 int i, j, k;
8
9 // Reserva de memoria
10 a = malloc(3*sizeof(int **));
11 for (i=0; i<3; i++) {
12 a[i] = malloc(3*sizeof(int *));
13 for (j=0; j<3; j++)
14 a[i][j] = malloc(3*sizeof(int));
15 }
16
17 // Inicializaci´on
18 for (i=0; i<3; i++)
19 for (j=0; j<3; j++)
20 for (k=0; k<3; k++)
21 a[i][j][k] = i+j+k;
22
242 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
23 // Impresi´on
24 for (i=0; i<3; i++)
25 for (j=0; j<3; j++)
26 for (k=0; k<3; k++)
27 printf ("%d %d %d: %dn", i, j, k, a[i][j][k]);
28
29 // Liberaci´on de memoria.
30 for (i=0; i<3; i++) {
31 for (j=0; j<3; j++)
32 free(a[i][j]);
33 free(a[i]);
34 }
35 free(a);
36 a = NULL;
37
38 return 0;
39 }
En la siguiente figura se muestra el estado de la memoria tras la inicializaci´on de la matriz
tridimensional:
a
0
0 1 2
1
0 1 2
2
0 1 2
0
0
1
1
2
2
1
0
2
1
3
2
2
0
3
1
4
2
1
0
2
1
3
2
2
0
3
1
4
2
3
0
4
1
5
2
2
0
3
1
4
2
3
0
4
1
5
2
4
0
5
1
6
2
4.4. Redimensionamiento de la reserva de memoria
Muchos programas no pueden determinar el tama˜no de sus vectores antes de empezar a trabajar
con ellos. Por ejemplo, cuando se inicia la ejecuci´on de un programa que gestiona una agenda
telef´onica no sabemos cu´antas entradas contendr´a finalmente. Podemos fijar un n´umero m´aximo
de entradas y pedir memoria para ellas con malloc, pero entonces estaremos reproduciendo el
problema que nos llev´o a presentar los vectores din´amicos. Afortunadamente, C permite que el
tama˜no de un vector cuya memoria se ha solicitado previamente con malloc crezca en funci´on
de las necesidades. Se usa para ello la funci´on realloc, cuyo prototipo es (similar a) ´este:
stdlib.h
1 ...
2 void * realloc(void * puntero, int bytes);
3 ...
Aqu´ı tienes un ejemplo de uso:
Introducci´on a la Programaci´on con C 243
4.4 Redimensionamiento de la reserva de memoria
1 #include <stdlib.h>
2
3 int main(void)
4 {
5 int * a;
6
7 a = malloc(10 * sizeof(int)); // Se pide espacio para 10 enteros.
8 ...
9 a = realloc(a, 20 * sizeof(int)); // Ahora se ampl´ıa para que quepan 20.
10 ...
11 a = realloc(a, 5 * sizeof(int)); // Y ahora se reduce a s´olo 5 (los 5 primeros).
12 ...
13 free(a);
14
15 return 0;
16 }
La funci´on realloc recibe como primer argumento el puntero que indica la zona de memoria que
deseamos redimensionar y como segundo argumento, el n´umero de bytes que deseamos asignar
ahora. La funci´on devuelve el puntero a la nueva zona de memoria.
¿Qu´e hace exactamente realloc? Depende de si se pide m´as o menos memoria de la que ya
se tiene reservada:
Si se pide m´as memoria, intenta primero ampliar la zona de memoria asignada. Si las
posiciones de memoria que siguen a las que ocupa a en ese instante est´an libres, se las
asigna a a, sin m´as. En caso contrario, solicita una nueva zona de memoria, copia el
contenido de la vieja zona de memoria en la nueva, libera la vieja zona de memoria y nos
devuelve el puntero a la nueva.
Si se pide menos memoria, libera la que sobra en el bloque reservado. Un caso extremo
consiste en llamar a realloc con una petici´on de 0 bytes. En tal caso, la llamada a realloc
es equivalente a free.
Al igual que malloc, si realloc no puede atender una petici´on, devuelve un puntero a NULL. Una
cosa m´as: si se llama a realloc con el valor NULL como primer par´ametro, realloc se comporta
como si se llamara a malloc.
Como puedes imaginar, un uso constante de realloc puede ser una fuente de ineficiencia.
Si tienes un vector que ocupa un 1 megabyte y usas realloc para que ocupe 1.1 megabyes, es
probable que provoques una copia de 1 megabyte de datos de la zona vieja a la nueva. Es m´as,
puede que ni siquiera tengas memoria suficiente para efectuar la nueva reserva, pues durante
un instante (mientras se efect´ua la copia) estar´as usando 2.1 megabytes.
Desarrollemos un ejemplo para ilustrar el concepto de reasignaci´on o redimensionamiento de
memoria. Vamos a dise˜nar un m´odulo que permita crear diccionarios de palabras. Un diccionario
es un vector de cadenas. Cuando creamos el diccionario, no sabemos cu´antas palabras albergar´a
ni qu´e longitud tiene cada una de las palabras. Tendremos que usar, pues, memoria din´amica.
Las palabras, una vez se introducen en el diccionario, no cambian de tama˜no, as´ı que bastar´a
con usar malloc para gestionar sus reservas de memoria. Sin embargo, la talla de la lista de
palabras s´ı var´ıa al a˜nadir palabras, as´ı que deberemos gestionarla con malloc/realloc.
Empecemos por definir el tipo de datos para un diccionario.
1 struct Diccionario {
2 char ** palabra;
3 int palabras;
4 };
Aqu´ı tienes un ejemplo de diccionario que contiene 4 palabras:
3
2
1
0
t a c o 0
m a n o 0
d a d i v o s o 0
a n u a l 0
palabra
palabras
4
244 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
Un diccionario vac´ıo no tiene palabra alguna ni memoria reservada. Esta funci´on crea un
diccionario:
1 struct Diccionario crea_diccionario(void)
2 {
3 struct Diccionario d;
4 d.palabra = NULL;
5 d.palabras = 0;
6 return d;
7 }
Ya podemos desarrollar la funci´on que inserta una palabra en el diccionario. Lo primero que
har´a la funci´on es comprobar si la palabra ya est´a en el diccionario. En tal caso, no har´a nada:
1 void inserta_palabra_en_diccionario(struct Diccionario * d, char pal[])
2 {
3 int i;
4
5 for (i=0; i<d->palabras; i++)
6 if (strcmp(d->palabra[i], pal)==0)
7 return;
8 ...
9 }
Si la palabra no est´a, hemos de a˜nadirla:
1 void inserta_palabra_en_diccionario(struct Diccionario * d, char pal[])
2 {
3 int i;
4
5 for (i=0; i<d->palabras; i++)
6 if (strcmp(d->palabra[i], pal)==0)
7 return;
8
9 d->palabra = realloc(d->palabra, (d->palabras+1) * sizeof(char *));
10 d->palabra[d->palabras] = malloc((strlen(pal)+1) * sizeof(char));
11 strcpy(d->palabra[d->palabras], pal);
12 d->palabras++;
13 }
Podemos liberar la memoria ocupada por un diccionario cuando no lo necesitamos m´as
llamando a esta otra funci´on:
1 void libera_diccionario(struct Diccionario * d)
2 {
3 int i;
4
5 if (d->palabra != NULL) {
6 for (i=0; i<d->palabras; i++)
7 free(d->palabra[i]);
8 free(d->palabra);
9 d->palabra = NULL;
10 d->palabras = 0;
11 }
12 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 239 Dise˜na una funci´on que devuelva cierto (valor 1) o falso (valor 0) en funci´on de si una
palabra pertenece o no a un diccionario.
· 240 Dise˜na una funci´on que borre una palabra del diccionario.
· 241 Dise˜na una funci´on que muestre por pantalla todas la palabras del diccionario que
empiezan por un prefijo dado (una cadena).
Introducci´on a la Programaci´on con C 245
4.4 Redimensionamiento de la reserva de memoria
No es lo mismo un puntero que un vector
Aunque C permite considerarlos una misma cosa en muchos contextos, hay algunas di-
ferencias entre un puntero a una serie de enteros, por ejemplo, y un vector de enteros.
Consideremos un programa con estas declaraciones:
1 int vector[10];
2 int escalar;
3 int * puntero;
4 int * otro_puntero;
A los punteros debe asign´arseles expl´ıcitamente alg´un valor:
• a la ((nada)): puntero = NULL;
• a memoria reservada mediante malloc: puntero = malloc(5*sizeof(int));
• a la direcci´on de memoria de una variable escalar del tipo al que puede apuntar
el puntero: puntero = &escalar;
• a la direcci´on de memoria en la que empieza un vector: puntero = vector;
• a la direcci´on de memoria de un elemento de un vector: puntero = &vector[2];
• a la direcci´on de memoria apuntada por otro puntero: puntero = otro_puntero;
• a una direcci´on calculada mediante aritm´etica de punteros: por ejemplo, pun-
tero = vector+2 es lo mismo que puntero = &vector[2].
Los vectores reservan memoria autom´aticamente, pero no puedes redimensionarlos.
Es ilegal, por ejemplo, una sentencia como ´esta: vector = puntero.
Eso s´ı, las funciones que admiten el paso de un vector v´ıa par´ametros, admiten tambi´en un
puntero y viceversa. De ah´ı que se consideren equivalentes.
Aunque suponga una simplificaci´on, puedes considerar que un vector es un puntero
inmutable (de valor constante).
· 242 Dise˜na una funci´on que muestre por pantalla todas la palabras del diccionario que
acaban con un sufijo dado (una cadena).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
La funci´on que determina si una palabra pertenece o no a un diccionario requiere tanto m´as
tiempo cuanto mayor es el n´umero de palabras del diccionario. Es as´ı porque el diccionario est´a
desordenado y, por tanto, la ´unica forma de estar seguros de que una palabra no est´a en el
diccionario es recorrer todas y cada una de las palabras (si, por contra, la palabra est´a en el
diccionario, no siempre es necesario recorrer el listado completo).
Podemos mejorar el comportamiento de la rutina de b´usqueda si mantenemos el dicciona-
rio siempre ordenado. Para ello hemos de modificar la funci´on de inserci´on de palabras en el
diccionario:
1 void inserta_palabra_en_diccionario(struct Diccionario *d, char pal[])
2 {
3 int i, j;
4
5 for (i=0; i<d->palabras; i++) {
6 if (strcmp(d->palabra[i], pal)==0) // Si ya est´a, no hay nada que hacer.
7 return;
8 if (strcmp(d->palabra[i], pal)>0) // Aqu´ı hemos encontrado su posici´on (orden alfab´etico)
9 break;
10 }
11
12 /* Si llegamos aqu´ı, la palabra no est´a y hay que insertarla en la posici´on i, desplazando
13 antes el resto de palabras. */
15
16 /* Reservamos espacio en la lista de palabras para una m´as. */
17 d->palabra = realloc(d->palabra, (d->palabras+1) * sizeof(char *));
18 /* Desplazamos el resto de palabras para que haya un hueco en el vector. */
19 for (j=d->palabras; j>i; j--) {
246 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
20 d->palabra[j] = malloc(strlen(d->palabra[j-1])+1)*sizeof(char));
21 strcpy(d->palabra[j], d->palabra[j-1]);
22 free(d->palabra[j-1]);
23 }
24 /* Y copiamos en su celda la nueva palabra */
25 d->palabra[i] = malloc( (strlen(pal)+1) * sizeof(char) );
26 strcpy(d->palabra[i], pal);
27 d->palabras++;
28 }
¡Buf! Las l´ıneas 20–22 no hacen m´as que asignar a una palabra el contenido de otra (la
que ocupa la posici´on j recibe una copia del contenido de la que ocupa la posici´on j-1). ¿No
hay una forma mejor de hacer eso mismo? S´ı. Transcribimos nuevamente las ´ultimas l´ıneas del
programa, pero con una sola sentencia que sustituye a las l´ıneas 20–22:
18 ...
19 for (j=d->palabras; j>i; i--)
20 d->palabra[j] = d->palabra[j-1];
21 /* Y copiamos en su celda la nueva palabra */
22 d->palabra[i] = malloc( (strlen(pal)+1) * sizeof(char) );
23 strcpy(d->palabra[i], pal);
24 d->palabras++;
25 }
No est´a mal, pero ¡no hemos pedido ni liberado memoria din´amica! ¡Ni siquiera hemos usado
strcpy, y eso que dijimos que hab´ıa que usar esa funci´on para asignar una cadena a otra. ¿C´omo
es posible? Antes hemos de comentar qu´e significa una asignaci´on como ´esta:
1 d->palabra[j] = d->palabra[j-1];
Significa que d->palabra[j] apunta al mismo lugar al que apunta d->palabra[j-1]. ¿Por qu´e?
Porque un puntero no es m´as que una direcci´on de memoria y asignar a un puntero el valor de
otro hace que ambos contengan la misma direcci´on de memoria, es decir, que ambos apunten
al mismo lugar.
Veamos qu´e pasa estudiando un ejemplo. Imagina un diccionario en el que ya hemos insertado
las palabras ((anual)), ((dadivoso)), ((mano)) y ((taco)) y que vamos a insertar ahora la palabra
((feliz)). Partimos, pues, de esta situaci´on:
3
2
1
0
t a c o 0
m a n o 0
d a d i v o s o 0
a n u a l 0
palabra
palabras
4
Una vez hemos redimensionado el vector de palabras, tenemos esto:
4
3
2
1
0
t a c o 0
m a n o 0
d a d i v o s o 0
a n u a l 0
palabra
palabras
4
La nueva palabra debe insertarse en la posici´on de ´ındice 2. El bucle ejecuta la asignaci´on
d->palabra[j] = d->palabra[j-1] para j tomando los valores 4 y 3. Cuando se ejecuta la
iteraci´on con j igual a 4, tenemos:
Introducci´on a la Programaci´on con C 247
4.4 Redimensionamiento de la reserva de memoria
4
3
2
1
0
t a c o 0
m a n o 0
d a d i v o s o 0
a n u a l 0
palabra
palabras
4
La ejecuci´on de la asignaci´on ha hecho que d->palabra[4] apunte al mismo lugar que d->palabra[3].
No hay problema alguno en que dos punteros apunten a un mismo bloque de memoria. En la
siguiente iteraci´on pasamos a esta otra situaci´on:
4
3
2
1
0
t a c o 0
m a n o 0
d a d i v o s o 0
a n u a l 0
palabra
palabras
4
Podemos reordenar gr´aficamente los elementos, para ver que, efectivamente, estamos haciendo
hueco para la nueva palabra:
4
3
2
1
0
t a c o 0
m a n o 0
d a d i v o s o 0
a n u a l 0
palabra
palabras
4
El bucle ha acabado. Ahora se pide memoria para el puntero d->palabra[i] (siendo i igual a
2). Se piden 6 bytes (((feliz)) tiene 5 caracteres m´as el terminador nulo):
4
3
2
1
0
t a c o 0
m a n o 0
d a d i v o s o 0
a n u a l 0
palabra
palabras
4
Y, finalmente, se copia en d->palabra[i] el contenido de pal con la funci´on strcpy y se incrementa
el valor de d->palabras:
4
3
2
1
0
t a c o 0
m a n o 0
f e l i z 0
d a d i v o s o 0
a n u a l 0
palabra
palabras
5
248 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
Podemos ahora implementar una funci´on de b´usqueda de palabras m´as eficiente. Una pri-
mera idea consiste en buscar desde el principio y parar cuando se encuentre la palabra buscada
o cuando se encuentre una palabra mayor (alfab´eticamente) que la buscada. En este ´ultimo
caso sabremos que la palabra no existe. Pero a´un hay una forma m´as eficiente de saber si una
palabra est´a o no en una lista ordenada: mediante una b´usqueda dicot´omica.
1 int buscar_en_diccionario(struct Diccionario d, char pal[])
2 {
3 int izquierda, centro, derecha;
4
5 izquierda = 0;
6 derecha = d.palabras;
7 while (izquierda < derecha) {
8 centro = (izquierda+derecha) / 2;
9 if (strcmp(pal, d.palabra[centro]) == 0)
10 return 1;
11 else if (strcmp(pal, d.palabra[centro]) < 0)
12 derecha = centro;
13 else
14 izquierda = centro+1;
15 }
16 return 0;
17 }
Podemos hacer una peque˜na mejora para evitar el sobrecoste de llamar dos veces a la funci´on
strcmp:
1 int buscar_en_diccionario(struct Diccionario d, char pal[])
2 {
3 int izquierda, centro, derecha, comparacion ;
4
5 izquierda = 0;
6 derecha = d.palabras;
7 while (izquierda < derecha) {
8 centro = (izquierda+derecha) / 2;
9 comparacion = strcmp(pal, d.palabra[centro]);
10 if (comparacion == 0)
11 return 1;
12 else if (comparacion < 0)
13 derecha = centro;
14 else
15 izquierda = centro+1;
16 }
17 return 0;
18 }
Juntemos todas las piezas y a˜nadamos una funci´on main que nos pida primero las palabras
del diccionario y, a continuaci´on, nos pida palabras que buscar en ´el:
diccionario.c diccionario.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4
5 #define MAXLON 80
6
7 struct Diccionario {
8 char ** palabra;
9 int palabras;
10 };
11
12 struct Diccionario crea_diccionario(void)
13 {
14 struct Diccionario d;
Introducci´on a la Programaci´on con C 249
4.4 Redimensionamiento de la reserva de memoria
15 d.palabra = NULL;
16 d.palabras = 0;
17 return d;
18 }
19
20 void libera_diccionario(struct Diccionario * d)
21 {
22 int i;
23
24 if (d->palabra != NULL) {
25 for (i=0; i<d->palabras; i++)
26 free(d->palabra[i]);
27 free(d->palabra);
28 d->palabra = NULL;
29 d->palabras = 0;
30 }
31 }
32
33 void inserta_palabra_en_diccionario(struct Diccionario *d, char pal[])
34 {
35 int i, j;
36
37 for (i=0; i<d->palabras; i++) {
38 if (strcmp(d->palabra[i], pal)==0) // Si ya est´a, no hay nada que hacer.
39 return;
40 if (strcmp(d->palabra[i], pal)>0) // Aqu´ı hemos encontrado su posici´on (orden alfab´etico)
41 break;
42 }
43
44 /* Si llegamos aqu´ı, la palabra no est´a y hay que insertarla en la posici´on i, desplazando
45 antes el resto de palabras. */
47
48 /* Reservamos espacio en la lista de palabras para una m´as. */
49 d->palabra = realloc(d->palabra, (d->palabras+1) * sizeof(char *));
50 /* Desplazamos el resto de palabras para que haya un hueco en el vector. */
51 for (j=d->palabras; j>i; j--) {
52 d->palabra[j] = malloc((strlen(d->palabra[j-1])+1)*sizeof(char));
53 strcpy(d->palabra[j], d->palabra[j-1]);
54 free(d->palabra[j-1]);
55 }
56 /* Y copiamos en su celda la nueva palabra */
57 d->palabra[i] = malloc( (strlen(pal)+1) * sizeof(char) );
58 strcpy(d->palabra[i], pal);
59 d->palabras++;
60 }
61
62 int buscar_en_diccionario(struct Diccionario d, char pal[])
63 {
64 int izquierda, centro, derecha, comparacion;
65
66 izquierda = 0;
67 derecha = d.palabras;
68 while (izquierda < derecha) {
69 centro = (izquierda+derecha) / 2;
70 comparacion = strcmp(pal, d.palabra[centro]);
71 if (comparacion == 0)
72 return 1;
73 else if (comparacion < 0)
74 derecha = centro;
75 else
76 izquierda = centro+1;
77 }
78 return 0;
250 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
79 }
80
81 int main(void)
82 {
83 struct Diccionario mi_diccionario;
84 int num_palabras;
85 char linea[MAXLON+1];
86
87 mi_diccionario = crea_diccionario();
88
89 printf ("
?
Cu´antas palabras tendr´a el diccionario?: ");
90 gets(linea); sscanf (linea, "%d", &num_palabras);
91 while (mi_diccionario.palabras != num_palabras) {
92 printf ("Palabra %d: ", mi_diccionario.palabras+1);
93 gets(linea);
94 inserta_palabra_en_diccionario(&mi_diccionario, linea);
95 }
96
97 do {
98 printf ("
?
Qu´e palabra busco? (pulsa retorno para acabar): ");
99 gets(linea);
100 if (strlen(linea) > 0) {
101 if (buscar_en_diccionario(mi_diccionario, linea))
102 printf ("S´ı que est´a.n");
103 else
104 printf ("No est´a.n");
105 }
106 } while(strlen(linea) > 0);
107
108 libera_diccionario(&mi_diccionario);
109
110 return 0;
111 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 243 ¿Cu´antas comparaciones se hacen en el peor de los casos en la b´usqueda dicot´omica de
una palabra cualquiera en un diccionario con 8 palabras? ¿Y en un diccionario con 16 palabras?
¿Y en uno con 32? ¿Y en uno con 1024? ¿Y en uno con 1048576? (Nota: el valor 1048576 es
igual a 220
.)
· 244 Al insertar una nueva palabra en un diccionario hemos de comprobar si exist´ıa previa-
mente y, si es una palabra nueva, averiguar en qu´e posici´on hay que insertarla. En la ´ultima
versi´on presentada, esa b´usqueda se efect´ua recorriendo el diccionario palabra a palabra. Mo-
dif´ıcala para que esa b´usqueda sea dicot´omica.
· 245 Dise˜na una funci´on que funda dos diccionarios ordenados en uno s´olo (tambi´en orde-
nado) que se devolver´a como resultado. La fusi´on se har´a de modo que las palabras que est´an
repetidas en ambos diccionarios aparezcan una s´ola vez en el diccionario final.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.4.1. Una aplicaci´on: una agenda telef´onica
Vamos a desarrollar un ejemplo completo: una agenda telef´onica. Utilizaremos vectores din´amicos
en diferentes estructuras de datos del programa. Por ejemplo, el nombre de las personas registra-
das en la agenda y sus n´umeros de tel´efonos consumir´an exactamente la memoria que necesitan,
sin desperdiciar un s´olo byte. Tambi´en el n´umero de entradas en la agenda se adaptar´a a las
necesidades reales de cada ejecuci´on. El nombre de una persona no cambia durante la ejecu-
ci´on del programa, as´ı que no necesitar´a redimensionamiento, pero la cantidad de n´umeros de
tel´efono de una misma persona s´ı (se pueden a˜nadir n´umeros de tel´efono a la entrada de una
persona que ya ha sido dada de alta). Gestionaremos esa memoria con redimensionamiento,
del mismo modo que usaremos redimensionamiento para gestionar el vector de entradas de la
agenda: gastaremos exactamente la memoria que necesitemos para almacenar la informaci´on.
Introducci´on a la Programaci´on con C 251
4.4 Redimensionamiento de la reserva de memoria
Aqu´ı tienes un texto con el tipo de informaci´on que deseamos almacenar:
Pepe P´erez 96 111111 96 369246
Ana Garc´ıa 964 321654 964 987654 964 001122
Juan Gil
Mar´ıa Paz 964 123456
Para que te hagas una idea del montaje, te mostramos la representaci´on gr´afica de las
estructuras de datos con las que representamos la agenda del ejemplo:
persona
personas
4
nombre
telefono
telefonos
2
0
nombre
telefono
telefonos
3
1
nombre
telefono
telefonos
0
2
nombre
telefono
telefonos
1
3
P
0
e
1
p
2
e
3 4
P
5
´e
6
r
7
e
8
z
9
0
10
A
0
n
1
a
2 3
G
4
a
5
r
6
c
7
´ı
8
a
9
0
10
J
0
u
1
a
2
n
3 4
G
5
i
6
l
7
0
8
M
0
a
1
r
2
´ı
3
a
4 5
P
6
a
7
z
8
0
9
0
0 1 2 3 4 5 6 7 8 9 10
9 6 4 1 2 3 4 5 6 0
0
0 1 2 3 4 5 6 7 8 9 10
1
0 1 2 3 4 5 6 7 8 9 10
2
0 1 2 3 4 5 6 7 8 9 10
9 6 4 3 2 1 6 5 4 0
9 6 4 9 8 7 6 5 4 0
9 6 4 0 0 1 1 2 2 0
0
0 1 2 3 4 5 6 7 8 9
1
0 1 2 3 4 5 6 7 8 9
9 6 1 1 1 1 1 1 0
9 6 3 6 9 2 4 6 0
Empezaremos proporcionando ((soporte)) para el tipo de datos ((entrada)): un nombre y un
listado de tel´efonos (un vector din´amico).
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 /************************************************************************
5 * Entradas
6 ************************************************************************/
8
9 struct Entrada {
10 char * nombre; // Nombre de la persona.
11 char ** telefono; // Vector din´amico de n´umeros de tel´efono.
12 int telefonos; // N´umero de elementos en el anterior vector.
13 };
14
15 void crea_entrada(struct Entrada * e, char * nombre)
16 /* Inicializa una entrada con un nombre. De momento, la lista de tel´efonos se pone a NULL. */
17 {
18 /* Reservamos memoria para el nombre y efectuamos la asignaci´on. */
19 e->nombre = malloc((strlen(nombre)+1)*sizeof(char));
20 strcpy(e->nombre, nombre);
252 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
21
22 e->telefono = NULL;
23 e->telefonos = 0;
24 }
25
26 void anyadir_telefono_a_entrada(struct Entrada * e, char * telefono)
27 {
28 e->telefono = realloc(e->telefono, (e->telefonos+1)*sizeof(char *));
29 e->telefono[e->telefonos] = malloc((strlen(telefono)+1)*sizeof(char));
30 strcpy(e->telefono[e->telefonos], telefono);
31 e->telefonos++;
32 }
33
34 void muestra_entrada(struct Entrada * e)
35 // Podr´ıamos haber pasado e por valor, pero resulta m´as eficiente (y no mucho m´as
36 // inc´omodo) hacerlo por referencia: pasamos as´ı s´olo 4 bytes en lugar de 12.
37 {
38 int i;
39
40 printf ("Nombre: %sn", e->nombre);
41 for(i=0; i<e->telefonos; i++)
42 printf (" Tel´efono %d: %sn", i+1, e->telefono[i]);
43 }
44
45 void libera_entrada(struct Entrada * e)
46 {
47 int i;
48
49 free(e->nombre);
50 for (i=0; i<e->telefonos; i++)
51 free(e->telefono[i]);
52 free(e->telefono);
53
54 e->nombre = NULL;
55 e->telefono = NULL;
56 e->telefonos = 0;
57 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 246 Modifica anyadir_telefono_a_entrada para que compruebe si el tel´efono ya hab´ıa sido
dado de alta. En tal caso, la funci´on dejar´a intacta la lista de tel´efonos de esa entrada.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Ya tenemos resuelta la gesti´on de entradas. Ocup´emonos ahora del tipo agenda y de su
gesti´on.
1 /************************************************************************
2 * Agenda
3 ************************************************************************/
5
6 struct Agenda {
7 struct Entrada * persona; /* Vector de entradas */
8 int personas; /* N´umero de entradas en el vector */
9 };
10
11 struct Agenda crea_agenda(void)
12 {
13 struct Agenda a;
14
15 a.persona = NULL;
16 a.personas = 0;
17 return a;
18 }
19
Introducci´on a la Programaci´on con C 253
4.4 Redimensionamiento de la reserva de memoria
20 void anyadir_persona(struct Agenda * a, char * nombre)
21 {
22 int i;
23
24 /* Averiguar si ya tenemos una persona con ese nombre */
25 for (i=0; i<a->personas; i++)
26 if (strcmp(a->persona[i].nombre, nombre) == 0)
27 return;
28
29 /* Si llegamos aqu´ı, es porque no ten´ıamos registrada a esa persona. */
30 a->persona = realloc(a->persona, (a->personas+1) * sizeof(struct Entrada));
31 crea_entrada(&a->persona[a->personas], nombre);
32 a->personas++;
33 }
34
35 void muestra_agenda(struct Agenda * a)
36 // Pasamos a as´ı por eficiencia.
37 {
38 int i;
39
40 for (i=0; i<a->personas; i++)
41 muestra_entrada(&a->persona[i]);
42 }
43
44 struct Entrada * buscar_entrada_por_nombre(struct Agenda * a, char * nombre)
45 {
46 int i;
47
48 for (i=0; i<a->personas; i++)
49 if (strcmp(a->persona[i].nombre, nombre)==0)
50 return & a->persona[i];
51
52 /* Si llegamos aqu´ı, no lo hemos encontrado. Devolvemos NULL para indicar que no
53 ((conocemos)) a esa persona. */
55 return NULL;
56 }
57
58 void libera_agenda(struct Agenda * a)
59 {
60 int i;
61
62 for (i=0; i<a->personas; i++)
63 libera_entrada(&a->persona[i]);
64 free(a->persona);
65 a->persona = NULL;
66 a->personas = 0;
67 }
F´ıjate en el prototipo de buscar_entrada_por_nombre: devuelve un puntero a un dato de tipo
struct Entrada. Es un truco bastante utilizado. Si no existe una persona con el nombre indicado,
se devuelve un puntero a NULL, y si existe, un puntero a esa persona. Ello nos permite, por
ejemplo, mostrarla a continuaci´on llamando a la funci´on que muestra ((entradas)), pues espera
un puntero a un struct Entrada. Ahora ver´as c´omo lo hacemos en el programa principal.
Ya podemos escribir el programa principal.
agenda.c agenda.c
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 #define MAXLON_NOMBRE 200
5 #define MAXLON_TELEFONO 40
6 #define MAXLON_LINEA 80
7
8 enum { Ver=1, AltaPersona, AnyadirTelefono, Buscar, Salir };
254 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
Un lenguaje para cada tarea
Acabas de ver que el tratamiento de cadenas en C es bastante primitivo y nos obliga a
estar pendientes de numerosos detalles. La memoria que ocupa cada cadena ha de ser
expl´ıcitamente controlada por el programador y las operaciones que podemos hacer con
ellas son, en principio, primitivas. Python, por contra, libera al programador de innumerables
preocupaciones cuando trabaja con objetos como las cadenas o los vectores din´amicos (sus
listas). Adem´as, ofrece ((de f´abrica)) numerosas utilidades para manipular cadenas (cortes,
funciones del m´odulo string, etc.) y listas (m´etodo append, sentencia del, cortes, ´ındices
negativos, etc.). ¿Por qu´e no usamos siempre Python? Por eficiencia. C permite dise˜nar, por
regla general, programas mucho m´as eficientes que sus equivalentes en Python. La mayor
flexibilidad de Python tiene un precio.
Antes de programar una aplicaci´on, hemos de preguntarnos si la eficiencia es un factor
cr´ıtico. Un programa con formularios (con un interfaz gr´afico) y/o accesos a una base de
datos funcionar´a probablemente igual de r´apido en C que en Python, ya que el cuello de
botella de la ejecuci´on lo introduce el propio usuario con su (lenta) velocidad de introducci´on
de datos y/o el sistema de base de datos al acceder a la informaci´on. En tal caso, parece
sensato escoger el lenguaje m´as flexible, el que permita desarrollar la aplicaci´on con mayor
facilidad. Un programa de c´alculo matricial, un sistema de adquisici´on de im´agenes para
una c´amara de v´ıdeo digital, etc. han de ejecutarse a una velocidad que (probablemente)
excluya a Python como lenguaje para la implementaci´on.
Hay lenguajes de programaci´on que combinan la eficiencia de C con parte de la flexi-
bilidad de Python y pueden suponer una buena soluci´on de compromiso en muchos casos.
Entre ellos hemos de destacar el lenguaje C++, que estudiar´as el pr´oximo curso.
Y hay una opci´on adicional: implementar en el lenguaje eficiente las rutinas de c´alculo
pesadas y usarlas desde un lenguaje de programaci´on flexible. Python ofrece un interfaz que
permite el uso de m´odulos escritos en C o C++. Su uso no es trivial, pero hay herramientas
como ((SWIG)) o ((Boost.Python)) que simplifican enormemente estas tareas.
9
.
.
.
133
134 /************************************************************************
135 * Programa principal
136 ************************************************************************/
138
139 int main(void)
140 {
141 struct Agenda miagenda;
142 struct Entrada * encontrada;
143 int opcion;
144 char nombre[MAXLON_NOMBRE+1];
145 char telefono[MAXLON_TELEFONO+1];
146 char linea[MAXLON_LINEA+1];
147
148 miagenda = crea_agenda();
149
150 do {
151 printf ("Men´u:n");
152 printf ("1) Ver contenido completo de la agenda.n");
153 printf ("2) Dar de alta una persona.n");
154 printf ("3) A~nadir un tel´efono.n");
155 printf ("4) Buscar tel´efonos de una persona.n");
156 printf ("5) Salir.n");
157 printf ("Opci´on: ");
158 gets(linea); sscanf (linea, "%d", &opcion);
159
160 switch(opcion) {
161
162 case Ver:
163 muestra_agenda(&miagenda);
Introducci´on a la Programaci´on con C 255
4.5 Introducci´on a la gesti´on de registros enlazados
164 break;
165
166 case AltaPersona:
167 printf ("Nombre: ");
168 gets(nombre);
169 anyadir_persona(&miagenda, nombre);
170 break;
171
172 case AnyadirTelefono:
173 printf ("Nombre: ");
174 gets(nombre);
175 encontrada = buscar_entrada_por_nombre(&miagenda, nombre);
176 if (encontrada == NULL) {
177 printf ("No hay nadie llamado %s en la agenda.n", nombre);
178 printf ("Por favor, d´e antes de alta a %s.n", nombre);
179 }
180 else {
181 printf ("Telefono: ");
182 gets(telefono);
183 anyadir_telefono_a_entrada(encontrada, telefono);
184 }
185 break;
186
187 case Buscar:
188 printf ("Nombre: ");
189 gets(nombre);
190 encontrada = buscar_entrada_por_nombre(&miagenda, nombre);
191 if (encontrada == NULL)
192 printf ("No hay nadie llamado %s en la agenda.n", nombre);
193 else
194 muestra_entrada(encontrada);
195 break;
196 }
197 } while (opcion != Salir);
198
199 libera_agenda(&miagenda);
200
201 return 0;
202 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 247 Dise˜na una funci´on que permita eliminar una entrada de la agenda a partir del nombre
de una persona.
· 248 La agenda, tal y como la hemos implementado, est´a desordenada. Modifica el programa
para que est´e siempre ordenada.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.5. Introducci´on a la gesti´on de registros enlazados
Hemos aprendido a crear vectores din´amicos. Podemos crear secuencias de elementos de cual-
quier tama˜no, aunque hemos visto que usar realloc para adaptar el n´umero de elementos re-
servados a las necesidades de la aplicaci´on es una posible fuente de ineficiencia, pues puede
provocar la copia de grandes cantidades de memoria. En esta secci´on aprenderemos a crear
listas con registros enlazados. Este tipo de listas ajustan su consumo de memoria al tama˜no de
la secuencia de datos que almacenan sin necesidad de llamar a realloc.
Una lista enlazada es una secuencia de registros unidos por punteros de manera que cada
registro contiene un valor y nos indica cu´al es el siguiente registro. As´ı pues, cada registro consta
de uno o m´as campos con datos y un puntero: el que apunta al siguiente registro. El ´ultimo
registro de la secuencia apunta a. . . nada. Aparte, un ((puntero maestro)) apunta al primero de
los registros de la secuencia. F´ıjate en este gr´afico para ir captando la idea:
256 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
Redimensionamiento con holgura
Estamos utilizando realloc para aumentar de celda en celda la reserva de memoria de los
vectores que necesitan crecer. Este tipo de redimensionamientos, tan ajustados a las ne-
cesidades exactas de cada momento, nos puede pasar factura: cada operaci´on realloc es
potencialmente lenta, pues ya sabes que puede disparar la copia de un bloque de memoria a
otro. Una t´ecnica para paliar este problema consiste en crecer varias celdas cada vez que nos
quedamos cortos de memoria en un vector. Un campo adicional en el registro, llam´emosle
capacidad, se usa para indicar cu´antas celdas tiene reservadas un vector y otro campo,
digamos, talla, indica cu´antas de dichas celdas est´an realmente ocupadas. Por ejemplo, este
vector, en el que s´olo estamos ocupando de momento tres celdas (las marcadas en negro),
tendr´ıa talla igual a 3 y capacidad igual a 5:
a
0 1 2 3 4
Cada vez que necesitamos escribir un nuevo dato en una celda adicional, comprobamos
si talla es menor o igual que capacidad. En tal caso, no hace falta redimensionar el vector,
basta con incrementar el valor de talla. Pero en caso contrario, nos curamos en salud y
redimensionamos pidiendo memoria para, pongamos, 10 celdas m´as (y, consecuentemente,
incrementamos el valor de capacidad en 10 unidades). De este modo reducimos el n´umero
de llamadas a realloc a una d´ecima parte. Incrementar un n´umero fijo de celdas no es la
´unica estrategia posible. Otra aproximaci´on consiste en duplicar la capacidad cada vez que
se precisa agrandar el vector. De este modo, el n´umero de llamadas a realloc es proporcional
al logaritmo en base 2 del n´umero de celdas del vector.
1 struct VDH { // vector Din´amico con Holgura (VDH)
2 int * dato;
3 int talla, capacidad;
4 };
5
6 struct VDH crea_VDH (void) {
7 struct VDH vdh;
8
9 vdh.dato = malloc(1*sizeof(int)); vdh.talla = 0; vdh.capacidad = 1;
10 return vdh;
11 }
12
13 void anyade_dato(struct VDH * vdh, int undato)
14 {
15 if (vdh->talla == vdh->capacidad) {
16 vdh->capacidad *= 2;
17 vdh->dato = realloc(vdh->dato, vdh->capacidad * sizeof(int));
18 }
19 vdh->dato[vdh->talla++] = undato;
20 }
21
22 void elimina_ultimo(struct VDH * vdh)
23 {
24 if (vdh->talla < vdh->capacidad/2 && vdh->capacidad > 0) {
25 vdh->capacidad /= 2;
26 vdh->dato = realloc(vdh->dato, vdh->capacidad * sizeof(int));
27 }
28 vdh->talla--;
29 }
Ciertamente, esta aproximaci´on ((desperdicia)) memoria, pero la cantidad de memoria
desperdiciada puede resultar tolerable para nuestra aplicaci´on. Python usa internamente
una variante de esta t´ecnica cuando modificamos la talla de una lista con, por ejemplo, el
m´etodo append (no duplica la memoria reservada, sino que la aumenta en cierta cantidad
constante).
Introducci´on a la Programaci´on con C 257
4.5 Introducci´on a la gesti´on de registros enlazados
lista 3
info sig
8
info sig
2
info sig
Conceptualmente, es lo mismo que se ilustra en este gr´afico:
lista 3
0
8
1
2
2
Pero s´olo conceptualmente. En la implementaci´on, el nivel de complejidad al que nos enfren-
tamos es mucho mayor. Eso s´ı, a cambio ganaremos en flexibilidad y podremos ofrecer ver-
siones eficientes de ciertas operaciones sobre listas de valores que implementadas con vectores
din´amicos ser´ıan muy costosas. Por otra parte, aprender estas t´ecnicas supone una inversi´on a
largo plazo: muchas estructuras din´amicas que estudiar´as en cursos de Estructuras de Datos se
basan en el uso de registros enlazados y, aunque mucho m´as complejas que las simples secuencias
de valores, usan las mismas t´ecnicas b´asicas de gesti´on de memoria que aprender´as ahora.
Para ir aprendiendo a usar estas t´ecnicas, gestionaremos ahora una lista con registros en-
lazados. La manejaremos directamente, desde un programa principal, y nos centraremos en la
realizaci´on de una serie de acciones que parten de un estado de la lista y la dejan en otro estado
diferente. Ilustraremos cada una de las acciones mostrando con todo detalle qu´e ocurre paso
a paso. En el siguiente apartado ((encapsularemos)) cada acci´on elemental (a˜nadir elemento,
borrar elemento, etc.) en una funci´on independiente.
4.5.1. Definici´on y creaci´on de la lista
Vamos a crear una lista de enteros. Empezamos por definir el tipo de los registros que componen
la lista:
1 struct Nodo {
2 int info;
3 struct Nodo * sig;
4 };
Un registro de tipo struct Nodo consta de dos elementos:
un entero llamado info, que es la informaci´on que realmente nos importa,
y un puntero a un elemento que es. . . ¡otro struct Nodo! (Observa que hay cierto nivel
de recursi´on o autoreferencia en la definici´on: un struct Nodo contiene un puntero a un
struct Nodo. Por esta raz´on, las estructuras que vamos a estudiar se denominan a veces
estructuras recursivas.)
Si quisi´eramos manejar una lista de puntos en el plano, tendr´ıamos dos opciones:
1. definir un registro con varios campos para la informaci´on relevante:
1 struct Nodo {
2 float x;
3 float y;
4 struct Nodo * sig;
5 };
lista
1.1
7.1
x
y
sig
0.2
0.1
x
y
sig
3.7
2.1
x
y
sig
2. definir un tipo adicional y utilizar un ´unico campo de dicho tipo:
1 struct Punto {
2 float x;
3 float y;
4 };
5
6 struct Nodo {
7 struct Punto info;
8 struct Nodo * sig;
9 };
258 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
lista
x
1.1
y
7.1
info sig
x
0.2
y
0.1
info sig
x
3.7
y
2.1
info sig
Cualquiera de las dos opciones es v´alida, si bien la segunda es m´as elegante.
Volvamos al estudio de nuestra lista de enteros. Creemos ahora el ((puntero maestro)), aqu´el
en el que empieza la lista de enteros:
1 int main(void)
2 {
3 struct Nodo * lista;
4
5 ...
No es m´as que un puntero a un elemento de tipo struct Nodo. Inicialmente, la lista est´a vac´ıa.
Hemos de indicarlo expl´ıcitamente as´ı:
1 int main(void)
2 {
3 struct Nodo * lista = NULL;
4
5 ...
Tenemos ahora esta situaci´on:
lista
O sea, lista no contiene nada, est´a vac´ıa.
4.5.2. Adici´on de nodos (por cabeza)
Empezaremos a˜nadiendo un nodo a la lista. Nuestro objetivo es pasar de la lista anterior a esta
otra:
lista 8
info sig
¿C´omo creamos el nuevo registro? Con malloc:
1 int main(void)
2 {
3 struct Nodo * lista = NULL;
4
5 lista = malloc( sizeof(struct Nodo) );
6 ...
´Este es el resultado:
lista
info sig
Ya tenemos el primer nodo de la lista, pero sus campos a´un no tienen los valores que deben
tener finalmente. Lo hemos representado gr´aficamente dejando el campo info en blanco y sin
poner una flecha que salga del campo sig.
Por una parte, el campo info deber´ıa contener el valor 8, y por otra, el campo sig deber´ıa
apuntar a NULL:
1 int main(void)
2 {
3 struct Nodo * lista = NULL;
4
5 lista = malloc( sizeof(struct Nodo) );
6 lista->info = 8;
7 lista->sig = NULL;
8 ...
Introducci´on a la Programaci´on con C 259
4.5 Introducci´on a la gesti´on de registros enlazados
No debe sorprenderte el uso del operador -> en las asignaciones a campos del registro. La
variable lista es de tipo struct Nodo *, es decir, es un puntero, y el operador -> permite
acceder al campo de un registro apuntado por un puntero. He aqu´ı el resultado:
lista 8
info sig
Ya tenemos una lista con un ´unico elemento.
Vamos a a˜nadir un nuevo nodo a la lista, uno que contenga el valor 3 y que ubicaremos
justo al principio de la lista, delante del nodo que contiene el valor 8. O sea, partimos de esta
situaci´on:
lista 8
info sig
y queremos llegar a esta otra:
lista 3
info sig
8
info sig
En primer lugar, hemos de crear un nuevo nodo al que deber´a apuntar lista. El campo sig
del nuevo nodo, por su parte, deber´ıa apuntar al nodo que contiene el valor 8. Empecemos por
la petici´on de un nuevo nodo que, ya que debe ser apuntado por lista, podemos pedir y rellenar
as´ı:
1 int main(void)
2 {
3 struct Nodo * lista = NULL;
4
5 ...
6 lista = malloc( sizeof(struct Nodo) );
7 lista->info = 3;
8 lista->sig = ???; // No sabemos c´omo expresar esta asignaci´on.
9 ...
¡Algo ha ido mal! ¿C´omo podemos asignar a lista->sig la direcci´on del siguiente nodo con valor
8? La situaci´on en la que nos encontramos se puede representar as´ı:
lista
3
info sig
8
info sig
¡No somos capaces de acceder al nodo que contiene el valor 8! Es lo que denominamos una p´erdida
de referencia, un grave error en nuestro programa que nos imposibilita seguir construyendo la
lista. Si no podemos acceder a un bloque de memoria que hemos pedido con malloc, tampoco
podremos liberarlo luego con free. Cuando se produce una p´erdida de referencia hay, pues,
una fuga de memoria: pedimos memoria al ordenador y no somos capaces de liberarla cuando
dejamos de necesitarla. Un programa con fugas de memoria corre el riesgo de consumir toda la
memoria disponible en el ordenador. Hemos de estar siempre atentos para evitar p´erdidas de
referencia. Es uno de los mayores peligros del trabajo con memoria din´amica.
¿C´omo podemos evitar la p´erdida de referencia? Muy f´acil: con un puntero auxiliar.
1 int main(void)
2 {
3 struct Nodo * lista = NULL, * aux ;
4
5 ...
6 aux = lista ;
7 lista = malloc( sizeof(struct Nodo) );
8 lista->info = 3;
9 lista->sig = aux ;
10 ...
260 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
La declaraci´on de la l´ınea 3 es curiosa. Cuando declaras dos o m´as punteros en una sola l´ınea,
has de poner el asterisco delante del identificador de cada puntero. En una l´ınea de declaraci´on
que empieza por la palabra int puedes declarar punteros a enteros y enteros, seg´un precedas
los respectivos identificadores con asterisco o no. Deteng´amonos un momento para considerar
el estado de la memoria justo despu´es de ejecutarse la l´ınea 6, que reza ((aux = lista)):
aux
lista 8
info sig
El efecto de la l´ınea 6 es que tanto aux como lista apuntan al mismo registro. La asignaci´on
de un puntero a otro hace que ambos apunten al mismo elemento. Recuerda que un puntero no
es m´as que una direcci´on de memoria y que copiar un puntero a otro hace que ambos contengan
la misma direcci´on de memoria, es decir, que ambos apunten al mismo lugar.
Sigamos con nuestra traza. Veamos c´omo queda la memoria justo despu´es de ejecutar la
l´ınea 7, que dice ((lista = malloc(sizeof(struct Nodo)))):
aux
lista
info sig
8
info sig
La l´ınea 8, que dice ((lista->info = 3)), asigna al campo info del nuevo nodo (apuntado por
lista) el valor 3:
aux
lista 3
info sig
8
info sig
La lista a´un no est´a completa, pero observa que no hemos perdido la referencia al ´ultimo
fragmento de la lista. El puntero aux la mantiene. Nos queda por ejecutar la l´ınea 9, que efect´ua
la asignaci´on ((lista->sig = aux)) y enlaza as´ı el campo sig del primer nodo con el segundo nodo,
el apuntado por aux. Tras ejecutarla tenemos:
aux
lista 3
info sig
8
info sig
¡Perfecto! ¿Seguro? ¿Y qu´e hace aux apuntando a´un a la lista? La verdad, nos da igual.
Lo importante es que los nodos que hay enlazados desde lista formen la lista que quer´ıamos
construir. No importa c´omo quedan los punteros auxiliares: una vez han desempe˜nado su funci´on
en la construcci´on de la lista, son sup´erfluos. Si te quedas m´as tranquilo, puedes a˜nadir una
l´ınea con aux = NULL al final del programa para que aux no quede apuntando a un nodo de la
lista, pero, repetimos, es innecesario.
4.5.3. Adici´on de un nodo (por cola)
Marqu´emonos un nuevo objetivo. Intentemos a˜nadir un nuevo nodo al final de la lista. Es decir,
partiendo de la ´ultima lista, intentemos obtener esta otra:
lista 3
info sig
8
info sig
2
info sig
¿Qu´e hemos de hacer? Para empezar, pedir un nuevo nodo, s´olo que esta vez no estar´a
apuntado por lista, sino por el que hasta ahora era el ´ultimo nodo de la lista. De momento,
lo mantendremos apuntado por un puntero auxiliar. Despu´es, accederemos de alg´un modo al
campo sig del ´ultimo nodo de la lista (el que tiene valor 8) y haremos que apunte al nuevo
nodo. Finalmente, haremos que el nuevo nodo contenga el valor 2 y que tenga como siguiente
nodo a NULL. Intent´emoslo:
Introducci´on a la Programaci´on con C 261
4.5 Introducci´on a la gesti´on de registros enlazados
1 int main(void)
2 {
3 struct Nodo * lista = NULL, * aux;
4
5 ...
6 aux = malloc( sizeof(struct Nodo) );
7 lista->sig->sig = aux ;
8 aux->info = 2;
9 aux->sig = NULL;
10
11 return 0;
12 }
Veamos c´omo queda la memoria paso a paso. Tras ejecutar la l´ınea 6 tenemos:
aux
info sig
lista 3
info sig
8
info sig
O sea, la lista que ((cuelga)) de lista sigue igual, pero ahora aux apunta a un nuevo nodo.
Pasemos a estudiar la l´ınea 7, que parece complicada porque contiene varias aplicaciones del
operador ->. Esa l´ınea reza as´ı: lista->sig->sig = aux. Vamos a ver qu´e significa ley´endola de
izquierda a derecha. Si lista es un puntero, y lista->sig es el campo sig del primer nodo, que es
otro puntero, entonces lista->sig->sig es el campo sig del segundo nodo, que es otro puntero.
Si a ese puntero le asignamos aux, el campo sig del segundo nodo apunta a donde apuntar´a
aux. Aqu´ı tienes el resultado:
aux
info sig
lista 3
info sig
8
info sig
A´un no hemos acabado. Una vez hayamos ejecutado las l´ıneas 8 y 9, el trabajo estar´a
completo:
aux 2
info sig
lista 3
info sig
8
info sig
¿Y es ´eso lo que busc´abamos? S´ı. Reordenemos gr´aficamente los diferentes componentes
para que su disposici´on en la imagen se asemeje m´as a lo que esper´abamos:
aux
lista 3
info sig
8
info sig
2
info sig
Ahora queda m´as claro que, efectivamente, hemos conseguido el objetivo. Esta figura y la
anterior son absolutamente equivalentes.
A´un hay algo en nuestro programa poco elegante: la asignaci´on ((lista->sig->sig = aux)) es
complicada de entender y da pie a un m´etodo de adici´on por el final muy poco ((extensible)).
¿Qu´e queremos decir con esto ´ultimo? Que si ahora queremos a˜nadir a la lista de 3 nodos
un cuarto nodo, tendremos que hacer ((lista->sig->sig->sig = aux)). Y si quisi´eramos a˜nadir
un quinto, ((lista->sig->sig->sig->sig = aux)) Imagina que la lista tiene 100 o 200 elementos.
¡Menuda complicaci´on proceder as´ı para a˜nadir por el final! ¿No podemos expresar la idea
((a˜nadir por el final)) de un modo m´as elegante y general? S´ı. Podemos hacer lo siguiente:
262 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
1. buscar el ´ultimo elemento con un bucle y mantenerlo referenciado con un puntero auxiliar,
digamos aux;
aux
lista 3
info sig
8
info sig
2. pedir un nodo nuevo y mantenerlo apuntado con otro puntero auxiliar, digamos nuevo;
aux
lista
nuevo
info sig
3
info sig
8
info sig
3. escribir en el nodo apuntado por nuevo el nuevo dato y hacer que su campo sig apunte a
NULL;
aux
lista
nuevo 2
info sig
3
info sig
8
info sig
4. hacer que el nodo apuntado por aux tenga como siguiente nodo al nodo apuntado por
nuevo.
aux
lista
nuevo 2
info sig
3
info sig
8
info sig
Lo que es equivalente a este otro gr´afico en el que, sencillamente, hemos reorganizado la
disposici´on de los diferentes elementos:
aux
lista
nuevo
3
info sig
8
info sig
2
info sig
Modifiquemos el ´ultimo programa para expresar esta idea:
1 int main(void)
2 {
3 struct Nodo * lista = NULL, * aux, * nuevo ;
4
5 ...
6 aux = lista;
7 while (aux->sig != NULL)
8 aux = aux->sig;
9 nuevo = malloc( sizeof(struct Nodo) );
10 nuevo->info = 2;
Introducci´on a la Programaci´on con C 263
4.5 Introducci´on a la gesti´on de registros enlazados
11 nuevo->sig = NULL;
12 aux->sig = nuevo ;
13
14 return 0;
15 }
La inicializaci´on y el bucle de las l´ıneas 6–8 buscan al ´ultimo nodo de la lista y lo mantienen
apuntado con aux. El ´ultimo nodo se distingue porque al llegar a ´el, aux->sig es NULL, de ah´ı la
condici´on del bucle. No importa cu´an larga sea la lista: tanto si tiene 1 elemento como si tiene
200, aux acaba apuntando al ´ultimo de ellos.5
Si partimos de una lista con dos elementos, ´este
es el resultado de ejecutar el bucle:
aux
lista
nuevo
3
info sig
8
info sig
Las l´ıneas 9–11 dejan el estado de la memoria as´ı:
aux
lista
nuevo 2
info sig
3
info sig
8
info sig
Finalmente, la l´ınea 12 completa la adici´on del nodo:
aux
lista
nuevo 2
info sig
3
info sig
8
info sig
Y ya est´a. Eso es lo que busc´abamos.
La inicializaci´on y el bucle de las l´ıneas 6–8 se pueden expresar en C de una forma mucho
m´as compacta usando una estructura for:
1 int main(void)
2 {
3 struct Nodo * lista = NULL, * aux, * nuevo;
4
5 ...
6 for (aux = lista; aux->sig != NULL; aux = aux->sig) ;
7 nuevo = malloc( sizeof(struct Nodo) );
8 nuevo->info = 2;
9 nuevo->sig = NULL;
10 aux->sig = nuevo;
11
12 return 0;
13 }
Observa que el punto y coma que aparece al final del bucle for hace que no tenga sentencia
alguna en su bloque:
1 for (aux = lista; aux->sig != NULL; aux = aux->sig) ;
5Aunque falla en un caso: si la lista est´a inicialmente vac´ıa. Estudiaremos este problema y su soluci´on m´as
adelante.
264 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
El bucle se limita a ((desplazar)) el puntero aux hasta que apunte al ´ultimo elemento de la lista.
Esta expresi´on del bucle que busca el elemento final es m´as propia de la programaci´on C, m´as
idiom´atica.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 249 Hemos dise˜nado un m´etodo (que mejoraremos en el siguiente apartado) que permite
insertar elementos por el final de una lista y hemos necesitado un bucle. ¿Har´a falta un bucle
para insertar un elemento por delante en una lista cualquiera? ¿C´omo har´ıas para convertir la
´ultima lista en esta otra?:
lista 1
info sig
3
info sig
8
info sig
2
info sig
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.5.4. Borrado de la cabeza
Vamos a aprender ahora a borrar elementos de una lista. Empezaremos por ver c´omo eliminar
el primer elemento de una lista. Nuestro objetivo es, partiendo de esta lista:
lista 3
info sig
8
info sig
2
info sig
llegar a esta otra:
lista 8
info sig
2
info sig
Como lo que deseamos es que lista pase a apuntar al segundo elemento de la lista, podr´ıamos
dise˜nar una aproximaci´on directa modificando el valor de lista:
1 int main(void)
2 {
3 struct Nodo * lista = NULL, * aux, * nuevo;
4
5 ...
6 lista = lista->sig; //
!
Mal! Se pierde la referencia a la cabeza original de la lista.
7
8 return 0;
9 }
El efecto obtenido por esa acci´on es ´este:
lista 3
info sig
8
info sig
2
info sig
Efectivamente, hemos conseguido que la lista apuntada por lista sea lo que pretend´ıamos,
pero hemos perdido la referencia a un nodo (el que hasta ahora era el primero) y ya no podemos
liberarlo. Hemos provocado una fuga de memoria.
Para liberar un bloque de memoria hemos de llamar a free con el puntero que apunta a la
direcci´on en la que empieza el bloque. Nuestro bloque est´a apuntado por lista, as´ı que podr´ıamos
pensar que la soluci´on es trivial y que bastar´ıa con llamar a free antes de modificar lista:
1 int main(void)
2 {
3 struct Nodo * lista = NULL, * aux, * nuevo;
4
5 ...
6 free(lista);
7 lista = lista->sig ; //
!
Mal! lista no apunta a una zona de memoria v´alida.
8
9 return 0;
10 }
Introducci´on a la Programaci´on con C 265
4.5 Introducci´on a la gesti´on de registros enlazados
Fugas de memoria, colapsos y recogida de basura
Muchos programas funcionan correctamente. . . durante un rato. Cuando llevan un tiempo
ejecut´andose, sin embargo, el ordenador empieza a ralentizarse sin explicaci´on aparente y
la memoria del ordenador se va agotando. Una de las razones para que esto ocurra son
las fugas de memoria. Si el programa pide bloques de memoria con malloc y no los libera
con free, ir´a consumiendo m´as y m´as memoria irremediablemente. Llegar´a un momento en
que no quede apenas memoria libre y la que quede, estar´a muy fragmentada, as´ı que las
peticiones a malloc costar´an m´as y m´as tiempo en ser satisfechas. . . ¡si es que pueden
ser satisfechas! La saturaci´on de la memoria provocada por la fuga acabar´a colapsando al
ordenador y, en algunos sistemas operativos, obligando a reiniciar la m´aquina.
El principal problema con las fugas de memoria es lo dif´ıciles de detectar que resultan.
Si pruebas el programa en un ordenador con mucha memoria, puede que no llegues a
apreciar efecto negativo alguno al efectuar pruebas. Dar por bueno un programa err´oneo
es, naturalmente, peor que saber que el programa a´un no es correcto.
Los lenguajes de programaci´on modernos suelen evitar las fugas de memoria propor-
cionando recogida de basura (del ingl´es garbage collection) autom´atica. Los sistemas de
recogida de basura detectan las p´erdidas de referencia (origen de las fugas de memoria) y
llaman autom´aticamente a free por nosotros. El programador s´olo escribe llamadas a malloc
(o la funci´on/mecanismo equivalente en su lenguaje) y el sistema se encarga de marcar como
disponibles los bloques de memoria no referenciados. Lenguajes como Python, Perl, Java,
Ruby, Tcl y un largo etc´etera tiene recogida de basura autom´atica, aunque todos deben la
idea a Lisp un lenguaje dise˜nado en los a˜nos 50 (¡¡¡!!!) que ya incorporaba esta ((avanzada))
caracter´ıstica.
Pero, claro, no iba a resultar tan sencillo. ¡La l´ınea 7, que dice ((lista = lista->sig)), no puede
ejecutarse! Tan pronto hemos ejecutado la l´ınea 6, tenemos otra fuga de memoria:
lista 8
info sig
2
info sig
O sea, hemos liberado correctamente el primer nodo, pero ahora hemos perdido la referencia
al resto de nodos y el valor de lista->sig est´a indefinido. ¿C´omo podemos arreglar esto? Si no
liberamos memoria, hay una fuga, y si la liberamos perdemos la referencia al resto de la lista.
La soluci´on es sencilla: guardamos una referencia al resto de la lista con un puntero auxiliar
cuando a´un estamos a tiempo.
1 int main(void)
2 {
3 struct Nodo * lista = NULL, * aux, * nuevo;
4
5 ...
6 aux = lista->sig ;
7 free(lista);
8 lista = aux;
9
10 return 0;
11 }
Ahora s´ı. Veamos paso a paso qu´e hacen las ´ultimas tres l´ıneas del programa. La asignaci´on
aux = lista->sig introduce una referencia al segundo nodo:
aux
lista 3
info sig
8
info sig
2
info sig
Al ejecutar free(lista), pasamos a esta otra situaci´on:
aux
lista 8
info sig
2
info sig
266 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
No hay problema. Seguimos sabiendo d´onde est´a el resto de la lista: ((cuelga)) de aux. As´ı
pues, podemos llegar al resultado deseado con la asignaci´on lista = aux:
aux
lista 8
info sig
2
info sig
¿Vas viendo ya el tipo de problemas al que nos enfrentamos con la gesti´on de listas? Los
siguientes apartados te presentan funciones capaces de inicializar listas, de insertar, borrar
y encontrar elementos, de mantener listas ordenadas, etc. Cada apartado te presentar´a una
variante de las listas enlazadas con diferentes prestaciones que permiten elegir soluciones de
compromiso entre velocidad de ciertas operaciones, consumo de memoria y complicaci´on de la
implementaci´on.
4.6. Listas con enlace simple
Vamos a desarrollar un m´odulo que permita manejar listas de enteros. En el fichero de cabecera
declararemos los tipos de datos b´asicos:
lista.h
struct Nodo {
int info;
struct Nodo * sig;
};
Como ya dijimos, este tipo de nodo s´olo alberga un n´umero entero. Si necesit´asemos una lista
de float deber´ıamos cambiar el tipo del valor del campo info. Y si quisi´esemos una lista de
((personas)), podr´ıamos a˜nadir varios campos a struct Nodo (uno para el nombre, otro para la
edad, etc.) o declarar info como de un tipo struct Persona definido previamente por nosotros.
Una lista es un puntero a un struct Nodo, pero cuesta poco definir un nuevo tipo para
referirnos con mayor brevedad al tipo ((lista)):
lista.h
...
typedef struct Nodo * TipoLista;
Ahora, podemos declarar una lista como struct Nodo * o como TipoLista, indistintamente.
Por claridad, nos referiremos al tipo de una lista con TipoLista y al de un puntero a un nodo
cualquiera con struct Nodo *, pero no olvides que ambos tipos son equivalentes.
Definici´on de struct con typedef
Hay quienes, para evitar la escritura repetida de la palabra struct, recurren a la inmediata
creaci´on de un nuevo tipo tan pronto se define el struct. Este c´odigo, por ejemplo, hace
eso:
1 typedef struct Nodo {
2 int info;
3 struct Nodo * sig;
4 } TipoNodo;
Como struct Nodo y TipoNodo son sin´onimos, pronto se intenta definir la estructura
as´ı:
1 typedef struct Nodo {
2 int info;
3 TipoNodo * sig; //
!
Mal!
4 } TipoNodo;
Pero el compilador emite un aviso de error. La raz´on es simple: la primera aparici´on de la
palabra TipoNodo tiene lugar antes de su propia definici´on.
Introducci´on a la Programaci´on con C 267
4.6 Listas con enlace simple
4.6.1. Creaci´on de lista vac´ıa
Nuestra primera funci´on crear´a una lista vac´ıa. El prototipo de la funci´on, que declaramos en
la cabecera lista.h, es ´este:
lista.h
...
extern TipoLista lista_vacia(void);
y la implementaci´on, que proporcionamos en una unidad de compilaci´on lista.c, resulta trivial:
lista.c
1 #include <stdlib.h>
2 #include "lista.h"
3
4 TipoLista lista_vacia(void)
5 {
6 return NULL;
7 }
La forma de uso es muy sencilla:
1 #include <stdlib.h>
2 #include "lista.h"
3
4 int main(void)
5 {
6 TipoLista lista;
7
8 lista = lista_vacia();
9
10 return 0;
11 }
Ciertamente podr´ıamos haber hecho lista = NULL, sin m´as, pero queda m´as elegante propor-
cionar funciones para cada una de las operaciones b´asicas que ofrece una lista, y crear una lista
vac´ıa es una operaci´on b´asica.
4.6.2. ¿Lista vac´ıa?
Nos vendr´a bien disponer de una funci´on que devuelva cierto o falso en funci´on de si la lista
est´a vac´ıa o no. El prototipo de la funci´on es:
lista.h
...
extern int es_lista_vacia(TipoLista lista);
y su implementaci´on, muy sencilla:
lista.c
1 int es_lista_vacia(TipoLista lista)
2 {
3 return lista == NULL;
4 }
4.6.3. Inserci´on por cabeza
Ahora vamos a crear una funci´on que inserta un elemento en una lista por la cabeza, es decir,
haciendo que el nuevo nodo sea el primero de la lista. Antes, veamos cu´al es el prototipo de la
funci´on:
lista.h
...
extern TipoLista inserta_por_cabeza(TipoLista lista, int valor);
268 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
La forma de uso de la funci´on ser´a ´esta:
miprograma.c
1 #include "lista.h"
2
3 int main(void)
4 {
5 TipoLista lista;
6
7 lista = lista_vacia();
8 lista = inserta_por_cabeza(lista, 2);
9 lista = inserta_por_cabeza(lista, 8);
10 lista = inserta_por_cabeza(lista, 3);
11 ...
12 return 0;
13 }
o, equivalentemente, esta otra:
miprograma.c
1 #include "lista.h"
2
3 int main(void)
4 {
5 TipoLista lista;
6
7 lista = inserta_por_cabeza(inserta_por_cabeza(inserta_por_cabeza(lista_vacia(),2),8),3);
8 ...
9 return 0;
10 }
Vamos con la implementaci´on de la funci´on. La funci´on debe empezar pidiendo un nuevo
nodo para el n´umero que queremos insertar.
lista.c
1 TipoLista inserta_por_cabeza(TipoLista lista, int valor)
2 {
3 struct Nodo * nuevo = malloc(sizeof(struct Nodo));
4
5 nuevo->info = valor;
6 ...
7 }
Ahora hemos de pensar un poco. Si lista va a tener como primer elemento a nuevo, ¿podemos
enlazar directamente lista con nuevo?
lista.c
1 TipoLista inserta_por_cabeza(TipoLista lista, int valor)@mal
2 {
3 struct Nodo * nuevo = malloc(sizeof(struct Nodo));
4
5 nuevo->info = valor;
6 lista = nuevo ;
7 ...
8 }
La respuesta es no. A´un no podemos. Si lo hacemos, no hay forma de enlazar nuevo->sig con
lo que era la lista anteriormente. Hemos perdido la referencia a la lista original. Ve´amoslo con
un ejemplo. Imagina una lista como ´esta:
lista 8
info sig
2
info sig
La ejecuci´on de la funci´on (incompleta) con valor igual a 3 nos lleva a esta otra situaci´on:
Introducci´on a la Programaci´on con C 269
4.6 Listas con enlace simple
nuevo 3
info sig
lista 8
info sig
2
info sig
Hemos perdido la referencia a la ((vieja)) lista. Una soluci´on sencilla consiste en, antes de modi-
ficar lista, asignar a nuevo->sig el valor de lista:
1 TipoLista inserta_por_cabeza(TipoLista lista, int valor)
2 {
3 struct Nodo * nuevo = malloc(sizeof(struct Nodo));
4
5 nuevo->info = valor;
6 nuevo->sig = lista ;
7 lista = nuevo;
8 return lista;
9 }
Tras ejecutarse la l´ınea 3, tenemos:
nuevo
info sig
lista 8
info sig
2
info sig
Las l´ıneas 5 y 6 modifican los campos del nodo apuntado por nuevo:
nuevo 3
info sig
lista 8
info sig
2
info sig
Finalmente, la l´ınea 7 hace que lista apunte a donde nuevo apunta. El resultado final es
´este:
nuevo 3
info sig
lista 8
info sig
2
info sig
S´olo resta redisponer gr´aficamente la lista para que no quepa duda de la correcci´on de la
soluci´on:
nuevo
lista 3
info sig
8
info sig
2
info sig
Hemos visto, pues, que el m´etodo es correcto cuando la lista no est´a vac´ıa. ¿Lo ser´a tambi´en
si suministramos una lista vac´ıa? La lista vac´ıa es un caso especial para el que siempre deberemos
considerar la validez de nuestros m´etodos.
Hagamos una comprobaci´on gr´afica. Si partimos de esta lista:
lista
y ejecutamos la funci´on (con valor igual a 10, por ejemplo), pasaremos moment´aneamente por
esta situaci´on:
nuevo 10
info sig
lista
270 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
y llegaremos, al final, a esta otra:
nuevo
lista 10
info sig
Ha funcionado correctamente. No tendremos tanta suerte con todas las funciones que vamos
a dise˜nar.
4.6.4. Longitud de una lista
Nos interesa conocer ahora la longitud de una lista. La funci´on que dise˜naremos recibe una lista
y devuelve un entero:
lista.h
...
extern int longitud_lista(TipoLista lista);
La implementaci´on se basa en recorrer toda la lista con un bucle que desplace un puntero
hasta llegar a NULL. Con cada salto de nodo a nodo, incrementaremos un contador cuyo valor
final ser´a devuelto por la funci´on:
lista.c
1 int longitud_lista(TipoLista lista)
2 {
3 struct Nodo * aux;
4 int contador = 0;
5
6 for (aux = lista; aux != NULL; aux = aux->sig)
7 contador++;
8 return contador;
9 }
Hagamos una peque˜na traza. Si recibimos esta lista:
lista 3
info sig
8
info sig
2
info sig
la variable contador empieza valiendo 0 y el bucle inicializa aux haciendo que apunte al primer
elemento:
aux
lista 3
info sig
8
info sig
2
info sig
En la primera iteraci´on, contador se incrementa en una unidad y aux pasa a apuntar al segundo
nodo:
aux
lista 3
info sig
8
info sig
2
info sig
Acto seguido, en la segunda iteraci´on, contador pasa a valer 2 y aux pasa a apuntar al tercer
nodo:
aux
lista 3
info sig
8
info sig
2
info sig
Finalmente, contador vale 3 y aux pasa a apuntar a NULL:
Introducci´on a la Programaci´on con C 271
4.6 Listas con enlace simple
aux
lista 3
info sig
8
info sig
2
info sig
Ah´ı acaba el bucle. El valor devuelto por la funci´on es 3, el n´umero de nodos de la lista.
Observa que longitud_lista tarda m´as cuanto mayor es la lista. Una lista con n nodos obliga
a efectuar n iteraciones del bucle for. Algo similar (aunque sin manejar listas enlazadas) nos
ocurr´ıa con strlen, la funci´on que calcula la longitud de una cadena.
La forma de usar esta funci´on desde el programa principal es sencilla:
miprograma.c
1 #include <stdio.h>
2 #include "lista.h"
3
4 int main(void)
5 {
6 TipoLista lista;
7 ...
8 printf ("Longitud: %dn", longitud_lista(lista));
9
10 return 0;
11 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 250 ¿Funcionar´a correctamente longitud_lista cuando le pasamos una lista vac´ıa?
· 251 Dise˜na una funci´on que reciba una lista de enteros con enlace simple y devuelva el valor
de su elemento m´aximo. Si la lista est´a vac´ıa, se devolver´a el valor 0.
· 252 Dise˜na una funci´on que reciba una lista de enteros con enlace simple y devuelva su
media. Si la lista est´a vac´ıa, se devolver´a el valor 0.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.6.5. Impresi´on en pantalla
Ahora que sabemos recorrer una lista no resulta en absoluto dif´ıcil dise˜nar un procedimiento
que muestre el contenido de una lista en pantalla. El prototipo es ´este:
lista.h
...
extern void muestra_lista(TipoLista lista);
y una posible implementaci´on, ´esta:
lista.c
1 void muestra_lista(TipoLista lista)
2 {
3 struct Nodo * aux;
4
5 for (aux = lista; aux != NULL; aux = aux->sig)
6 printf ("%dn", aux->info);
7 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 253 Dise˜na un procedimiento que muestre el contenido de una lista al estilo Python. Por
ejemplo, la lista de la ´ultima figura se mostrar´a como [3, 8, 2]. F´ıjate en que la coma s´olo
aparece separando a los diferentes valores, no despu´es de todos los n´umeros.
· 254 Dise˜na un procedimiento que muestre el contenido de una lista como se indica en el
siguiente ejemplo. La lista formada por los valores 3, 8 y 2 se representar´a as´ı:
->[3]->[8]->[2]->|
(La barra vertical representa a NULL.)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
272 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
4.6.6. Inserci´on por cola
Dise˜nemos ahora una funci´on que inserte un nodo al final de una lista. Su prototipo ser´a:
lista.h
...
extern TipoLista inserta_por_cola(TipoLista lista, int valor);
Nuestra funci´on se dividir´a en dos etapas: una primera que localice al ´ultimo elemento de
la lista, y otra que cree el nuevo nodo y lo una a la lista.
Aqu´ı tienes la primera etapa:
E lista.c E
1 TipoLista inserta_por_cola(TipoLista lista, int valor)
2 {
3 struct Nodo * aux;
4
5 for (aux = lista; aux->sig != NULL; aux = aux->sig) ;
6 ...
7 }
Analicemos paso a paso el bucle con un ejemplo. Imagina que la lista que nos suministran
en lista ya tiene tres nodos:
lista 3
info sig
8
info sig
2
info sig
La primera iteraci´on del bucle hace que aux apunte al primer elemento de la lista:
aux
lista 3
info sig
8
info sig
2
info sig
Habr´a una nueva iteraci´on si aux->sig es distinto de NULL, es decir, si el nodo apuntado por
aux no es el ´ultimo de la lista. Es nuestro caso, as´ı que iteramos haciendo aux = aux->sig, o
sea, pasamos a esta nueva situaci´on:
aux
lista 3
info sig
8
info sig
2
info sig
¿Sigue siendo cierto que aux->sig es distinto de NULL? S´ı. Avanzamos aux un nodo m´as a la
derecha:
aux
lista 3
info sig
8
info sig
2
info sig
¿Y ahora? ¿Es cierto que aux->sig es distinto de NULL? No, es igual a NULL. Ya hemos
llegado al ´ultimo nodo de la lista. F´ıjate en que hemos parado un paso antes que cuando
cont´abamos el n´umero de nodos de una lista; entonces la condici´on de iteraci´on del bucle era
otra: ((aux != NULL)).
Podemos proceder con la segunda fase de la inserci´on: pedir un nuevo nodo y enlazarlo desde
el actual ´ultimo nodo. Nos vendr´a bien un nuevo puntero auxiliar:
E lista.c E
1 TipoLista inserta_por_cola(TipoLista lista, int valor)
2 {
3 struct Nodo * aux, * nuevo ;
4
5 for (aux = lista; aux->sig != NULL; aux = aux->sig) ;
Introducci´on a la Programaci´on con C 273
4.6 Listas con enlace simple
6 nuevo = malloc(sizeof(struct Nodo));
7 nuevo->info = valor;
8 nuevo->sig = NULL;
9 aux->sig = nuevo;
10 return lista;
11 }
El efecto de la ejecuci´on de las nuevas l´ıneas, suponiendo que el valor es 10, es ´este:
nuevo 10
info sig
aux
lista 3
info sig
8
info sig
2
info sig
Est´a claro que ha funcionado correctamente, ¿no? Tal vez resulte de ayuda ver la misma es-
tructura reordenada as´ı:
lista 3
info sig
8
info sig
2
info sig
10
info sig
aux nuevo
Bien, entonces, ¿por qu´e hemos marcado la funci´on como incorrecta? Veamos qu´e ocurre si
la lista que nos proporcionan est´a vac´ıa. Si la lista est´a vac´ıa, lista vale NULL. En la primera
iteraci´on del bucle for asignaremos a aux el valor de lista, es decir, NULL. Para ver si pasamos a
efectuar la primera iteraci´on, hemos de comprobar antes si aux->sig es distinto de NULL. ¡Pero
es un error preguntar por el valor de aux->sig cuando aux es NULL! Un puntero a NULL no
apunta a nodo alguno, as´ı que no podemos preguntar por el valor del campo sig de un nodo
que no existe. ¡Ojo con este tipo de errores!: los accesos a memoria que no nos ((pertenece))
no son detectables por el compilador. Se manifiestan en tiempo de ejecuci´on y, normalmente,
con consecuencias desastrosas6
, especialmente al efectuar escrituras de informaci´on. ¿C´omo
podemos corregir la funci´on? Tratando a la lista vac´ıa como un caso especial:
lista.c
1 TipoLista inserta_por_cola(TipoLista lista, int valor)
2 {
3 struct Nodo * aux, * nuevo;
4
5 if (lista == NULL) {
6 lista = malloc(sizeof(struct Nodo));
7 lista->info = valor;
8 lista->sig = NULL;
9 }
10 else {
11 for (aux = lista; aux->sig != NULL; aux = aux->sig) ;
12 nuevo = malloc(sizeof(struct Nodo));
13 nuevo->info = valor;
14 nuevo->sig = NULL;
15 aux->sig = nuevo;
16 }
17 return lista;
18 }
Como puedes ver, el tratamiento de la lista vac´ıa es muy sencillo, pero especial. Ya te lo
advertimos antes: comprueba siempre si tu funci´on se comporta adecuadamente en situaciones
extremas. La lista vac´ıa es un caso para el que siempre deber´ıas comprobar la validez de tu
aproximaci´on.
La funci´on puede retocarse factorizando acciones comunes a los dos bloques del if-else:
6En Linux, por ejemplo, obtendr´as un error (t´ıpicamente ((Segmentation fault))) y se abortar´a inmediatamente
la ejecuci´on del programa. En Microsoft Windows es frecuente que el ordenador ((se cuelgue)).
274 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
lista.c
1 TipoLista inserta_por_cola(TipoLista lista, int valor)
2 {
3 struct Nodo * aux, * nuevo;
4
5 nuevo = malloc(sizeof(struct Nodo));
6 nuevo->info = valor;
7 nuevo->sig = NULL;
8 if (lista == NULL)
9 lista = nuevo;
10 else {
11 for (aux = lista; aux->sig != NULL; aux = aux->sig) ;
12 aux->sig = nuevo;
13 }
14 return lista;
15 }
Mejor as´ı.
4.6.7. Borrado de la cabeza
La funci´on que vamos a dise˜nar ahora recibe una lista y devuelve esa misma lista sin el nodo
que ocupaba inicialmente la posici´on de cabeza. El prototipo ser´a ´este:
lista.h
...
extern TipoLista borra_cabeza(TipoLista lista);
Implement´emosla. No podemos hacer simplemente lista = lista->sig. Ciertamente, ello con-
seguir´ıa que, en principio, los nodos que ((cuelgan)) de lista formaran una lista correcta, pero
estar´ıamos provocando una fuga de memoria al no liberar con free el nodo de la cabeza (ya lo
vimos cuando introdujimos las listas enlazadas):
lista 3
info sig
8
info sig
2
info sig
Tampoco podemos empezar haciendo free(lista) para liberar el primer nodo, pues entonces
perder´ıamos la referencia al resto de nodos. La memoria quedar´ıa as´ı:
lista 3
info sig
8
info sig
2
info sig
¿Qui´en apuntar´ıa entonces al primer nodo de la lista?
La soluci´on requiere utilizar un puntero auxiliar:
E lista.c E
1 TipoLista borra_cabeza(TipoLista lista)
2 {
3 struct Nodo * aux;
4
5 aux = lista->sig;
6 free(lista);
7 lista = aux;
8 return lista;
9 }
Ahora s´ı, ¿no? No. Falla en el caso de que lista valga NULL, es decir, cuando nos pasan una
lista vac´ıa. La asignaci´on aux = lista->sig es err´onea si lista es NULL. Pero la soluci´on es muy
sencilla en este caso: si nos piden borrar el nodo de cabeza de una lista vac´ıa, ¿qu´e hemos de
hacer? ¡Absolutamente nada!:
lista.c
1 TipoLista borra_cabeza(TipoLista lista)
2 {
Introducci´on a la Programaci´on con C 275
4.6 Listas con enlace simple
3 struct Nodo * aux;
4
5 if (lista != NULL) {
6 aux = lista->sig;
7 free(lista);
8 lista = aux;
9 }
10 return lista;
11 }
Tenlo siempre presente: si usas la expresi´on aux->sig para cualquier puntero aux, has de
estar completamente seguro de que aux no es NULL.
4.6.8. Borrado de la cola
Vamos a dise˜nar ahora una funci´on que elimine el ´ultimo elemento de una lista. He aqu´ı su
prototipo:
lista.h
...
extern TipoLista borra_cola(TipoLista lista);
Nuevamente, dividiremos el trabajo en dos fases:
1. localizar el ´ultimo nodo de la lista para liberar la memoria que ocupa,
2. y hacer que el hasta ahora pen´ultimo nodo tenga como valor de su campo sig a NULL.
La primera fase consistir´a b´asicamente en esto:
E lista.c E
1 TipoLista borra_cola(TipoLista lista)
2 {
3 struct Nodo * aux;
4
5 for (aux = lista; aux->sig != NULL; aux = aux->sig) ;
6 ...
7 }
¡Alto! Este mismo bucle ya nos di´o problemas cuando tratamos de insertar por la cola: no
funciona correctamente con listas vac´ıas. De todos modos, el problema tiene f´acil soluci´on: no
tiene sentido borrar nada de una lista vac´ıa.
E lista.c E
1 TipoLista borra_cola(TipoLista lista)
2 {
3 struct Nodo * aux;
4
5 if (lista != NULL) {
6 for (aux = lista; aux->sig != NULL; aux = aux->sig) ;
7 ...
8 }
9 return lista;
10 }
Ahora el bucle solo se ejecuta con listas no vac´ıas. Si partimos de esta lista:
aux
lista 3
info sig
8
info sig
2
info sig
el bucle hace que aux acabe apuntando al ´ultimo nodo:
276 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
aux
lista 3
info sig
8
info sig
2
info sig
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 255 ¿Seguro que el bucle de borra_cola funciona correctamente siempre? Piensa si hace lo
correcto cuando se le pasa una lista formada por un solo elemento.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Si hemos localizado ya el ´ultimo nodo de la lista, hemos de liberar su memoria:
E lista.c E
1 TipoLista borra_cola(TipoLista lista)
2 {
3 struct Nodo * aux;
4
5 if (lista != NULL) {
6 for (aux = lista; aux->sig != NULL; aux = aux->sig) ;
7 free(aux);
8 ...
9 }
10 }
Llegamos as´ı a esta situaci´on:
aux
lista 3
info sig
8
info sig
F´ıjate: s´olo nos falta conseguir que el nuevo ´ultimo nodo (el de valor igual a 8) tenga como
valor del campo sig a NULL. Problema: ¿y c´omo sabemos cu´al es el ´ultimo nodo? No se puede
saber. Ni siquiera utilizando un nuevo bucle de b´usqueda del ´ultimo nodo, ya que dicho bucle
se basaba en que el ´ultimo nodo es reconocible porque tiene a NULL como valor de sig, y ahora
el ´ultimo no apunta con sig a NULL.
El ((truco)) consiste en usar otro puntero auxiliar y modificar el bucle de b´usqueda del ´ultimo
para haga que el nuevo puntero auxiliar vaya siempre ((un paso por detr´as)) de aux. Observa:
E lista.c E
1 TipoLista borra_cola(TipoLista lista)
2 {
3 struct Nodo * aux, * atras ;
4
5 if (lista != NULL) {
6 for (atras = NULL, aux = lista; aux->sig != NULL; atras = aux, aux = aux->sig) ;
7 free(aux);
8 ...
9 }
10 }
F´ıjate en el nuevo aspecto del bucle for. Utilizamos una construcci´on sint´actica que a´un no
conoces, as´ı que nos detendremos brevemente para explicarla. Los bucles for permiten trabajar
con m´as de una inicializaci´on y con m´as de una acci´on de paso a la siguiente iteraci´on. Este
bucle, por ejemplo, trabaja con dos variables enteras, una que toma valores crecientes y otra
que toma valores decrecientes:
1 for (i=0, j=10; i<3; i++, j--)
2 printf ("%d %dn", i, j);
¡Ojo! Es un ´unico bucle, no son dos bucles anidados. ¡No te confundas! Las diferentes inicia-
lizaciones y pasos de iteraci´on se separan con comas. Al ejecutarlo, por pantalla aparecer´a
esto:
0 10
1 9
2 8
Introducci´on a la Programaci´on con C 277
4.6 Listas con enlace simple
Sigamos con el problema que nos ocupa. Veamos, paso a paso, qu´e hace ahora el bucle. En
la primera iteraci´on tenemos:
atras aux
lista 3
info sig
8
info sig
2
info sig
Y en la segunda iteraci´on:
atras aux
lista 3
info sig
8
info sig
2
info sig
Y en la tercera:
atras aux
lista 3
info sig
8
info sig
2
info sig
¿Ves? No importa cu´an larga sea la lista; el puntero atras siempre va un paso por detr´as del
puntero aux. En nuestro ejemplo ya hemos llegado al final de la lista, as´ı que ahora podemos
liberar el nodo apuntado por aux:
atras aux
lista 3
info sig
8
info sig
Ahora podemos continuar: ya hemos borrado el ´ultimo nodo, pero esta vez s´ı que sabemos cu´al
es el nuevo ´ultimo nodo.
E lista.c E
1 TipoLista borra_cola(TipoLista lista)
2 {
3 struct Nodo * aux, * atras ;
4
5 if (lista != NULL) {
6 for (atras = NULL, aux = lista; aux->sig != NULL; atras = aux, aux = aux->sig) ;
7 free(aux);
8 atras->sig = NULL;
9 ...
10 }
11 }
Tras ejecutar la nueva sentencia, tenemos:
atras aux
lista 3
info sig
8
info sig
A´un no hemos acabado. La funci´on borra_cola trabaja correctamente con la lista vac´ıa,
pues no hace nada en ese caso (no hay nada que borrar), pero, ¿funciona correctamente cuando
suministramos una lista con un ´unico elemento? Hagamos una traza.
Tras ejecutar el bucle que busca a los elementos ´ultimo y pen´ultimo, los punteros atras y
aux quedan as´ı:
atras aux
lista 3
info sig
278 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
Ahora se libera el nodo apuntado por aux:
atras aux
lista
Y, finalmente, hacemos que atras->sig sea igual NULL. Pero, ¡eso es imposible! El puntero
atras apunta a NULL, y hemos dicho ya que NULL no es un nodo y, por tanto, no tiene campo
alguno.
Tratemos este caso como un caso especial. En primer lugar, ¿c´omo podemos detectarlo?
Viendo si atras vale NULL. ¿Y qu´e hemos de hacer entonces? Hemos de hacer que lista pase a
valer NULL, sin m´as.
lista.c
1 TipoLista borra_cola(TipoLista lista)
2 {
3 struct Nodo * aux, * atras ;
4
5 if (lista != NULL) {
6 for (atras = NULL, aux = lista; aux->sig != NULL; atras = aux, aux = aux->sig) ;
7 free(aux);
8 if (atras == NULL)
9 lista = NULL;
10 else
11 atras->sig = NULL;
12 }
13 return lista;
14 }
Ya est´a. Si aplic´asemos este nuevo m´etodo, nuestro ejemplo concluir´ıa as´ı:
atras aux
lista
Hemos aprendido una lecci´on: otro caso especial que conviene estudiar expl´ıcitamente es el
de la lista compuesta por un solo elemento.
Insistimos en que debes seguir una sencilla regla en el dise˜no de funciones con punteros: si
accedes a un campo de un puntero ptr, por ejemplo, ptr->sig o ptr->info, preg´untate siempre si
cabe alguna posibilidad de que ptr sea NULL; si es as´ı, tienes un problema que debes solucionar.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 256 ¿Funcionan correctamente las funciones que hemos definido antes (c´alculo de la lon-
gitud, inserci´on por cabeza y por cola y borrado de cabeza) cuando se suministra una lista
compuesta por un ´unico elemento?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.6.9. B´usqueda de un elemento
Vamos a dise˜nar ahora una funci´on que no modifica la lista. Se trata de una funci´on que nos
indica si un valor entero pertenece a la lista o no. El prototipo de la funci´on ser´a ´este:
lista.h
...
extern int pertenece(TipoLista lista, int valor);
La funci´on devolver´a 1 si valor est´a en la lista y 0 en caso contrario.
¿Qu´e aproximaci´on seguiremos? Pues la misma que segu´ıamos con los vectores: recorrer cada
uno de sus elementos y, si encontramos uno con el valor buscado, devolver inmediatamente el
valor 1; si llegamos al final de la lista, ser´a que no lo hemos encontrado, as´ı que en tal caso
devolveremos el valor 0.
Introducci´on a la Programaci´on con C 279
4.6 Listas con enlace simple
lista.c
1 int pertenece(TipoLista lista, int valor)
2 {
3 struct Nodo * aux;
4
5 for (aux=lista; aux != NULL; aux = aux->sig)
6 if (aux->info == valor)
7 return 1;
8 return 0;
9 }
´Esta ha sido f´acil, ¿no?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 257 ¿Funciona correctamente pertenece cuando se suministra NULL como valor de lista, es
decir, cuando se suministra una lista vac´ıa? ¿Y cuando se suministra una lista con un ´unico
elemento?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.6.10. Borrado del primer nodo con un valor determinado
El problema que abordamos ahora es el dise˜no de una funci´on que recibe una lista y un valor
y elimina el primer nodo de la lista cuyo campo info coincide con el valor.
lista.h
...
extern TipoLista borra_primera_ocurrencia(TipoLista lista, int valor);
Nuestro primer problema consiste en detectar el valor en la lista. Si el valor no est´a en la
lista, el problema se resuelve de forma trivial: se devuelve la lista intacta y ya est´a.
E lista.c E
1 TipoLista borra_primera_ocurrencia(TipoLista lista, int valor)
2 {
3 struct Nodo * aux;
4
5 for (aux=lista; aux != NULL; aux = aux->sig)
6 if (aux->info == valor) {
7 ...
8 }
9 return lista;
10
11 }
Veamos con un ejemplo en qu´e situaci´on estamos cuando llegamos a la l´ınea marcada con
puntos suspensivos. En esta lista hemos buscado el valor 8, as´ı que podemos representar la
memoria as´ı:
aux
lista 3
info sig
8
info sig
2
info sig
Nuestro objetivo ahora es, por una parte, efectuar el siguiente ((empalme)) entre nodos:
aux
lista 3
info sig
8
info sig
2
info sig
y, por otra, eliminar el nodo apuntado por aux:
280 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
aux
lista 3
info sig
2
info sig
Problema: ¿c´omo hacemos el ((empalme))? Necesitamos conocer cu´al es el nodo que precede
al que apunta aux. Eso sabemos hacerlo con ayuda de un puntero auxiliar que vaya un paso
por detr´as de aux:
E lista.c E
1 TipoLista borra_primera_ocurrencia(TipoLista lista, int valor)
2 {
3 struct Nodo * aux, * atras ;
4
5 for (atras = NULL, aux=lista; aux != NULL; atras = aux , aux = aux->sig)
6 if (aux->info == valor) {
7 atras->sig = aux->sig;
8 ...
9 }
10 return lista;
11 }
El puntero atras empieza apuntando a NULL y siempre va un paso por detr´as de aux.
atras aux
lista 3
info sig
8
info sig
2
info sig
Es decir, cuando aux apunta a un nodo, atras apunta al anterior. La primera iteraci´on
cambia el valor de los punteros y los deja en este estado:
atras aux
lista 3
info sig
8
info sig
2
info sig
¿Es correcta la funci´on? Hay una fuente de posibles problemas. Estamos asignando algo a
atras->sig. ¿Cabe alguna posibilidad de que atras sea NULL? S´ı. El puntero atras es NULL cuando
el elemento encontrado ocupa la primera posici´on. F´ıjate en este ejemplo en el que queremos
borrar el elemento de valor 3:
atras aux
lista 3
info sig
8
info sig
2
info sig
El ((empalme)) procedente en este caso es ´este:
atras aux
lista 3
info sig
8
info sig
2
info sig
lista.c
1 TipoLista borra_primera_ocurrencia(TipoLista lista, int valor)
2 {
3 struct Nodo * aux, * atras ;
4
5 for (atras = NULL, aux=lista; aux != NULL; atras = aux, aux = aux->sig)
6 if (aux->info == valor) {
7 if (atras == NULL)
Introducci´on a la Programaci´on con C 281
4.6 Listas con enlace simple
8 lista = aux->sig;
9 else
10 atras->sig = aux->sig;
11 ...
12 }
13 return lista;
14 }
Ahora podemos borrar el elemento apuntado por aux con tranquilidad y devolver la lista mo-
dificada:
lista.c
1 TipoLista borra_primera_ocurrencia(TipoLista lista, int valor)
2 {
3 struct Nodo * aux, * atras ;
4
5 for (atras = NULL, aux=lista; aux != NULL; atras = aux, aux = aux->sig)
6 if (aux->info == valor) {
7 if (atras == NULL)
8 lista = aux->sig;
9 else
10 atras->sig = aux->sig;
11 free(aux);
12 return lista;
13 }
14 return lista;
15 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 258 ¿Funciona borra_primera_ocurrencia cuando ning´un nodo de la lista contiene el valor
buscado?
· 259 ¿Funciona correctamente en los siguientes casos?
lista vac´ıa;
lista con un s´olo elemento que no coincide en valor con el buscado;
lista con un s´olo elemento que coincide en valor con el buscado.
Si no es as´ı, corrige la funci´on.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.6.11. Borrado de todos los nodos con un valor dado
Borrar todos los nodos con un valor dado y no s´olo el primero es bastante m´as complicado,
aunque hay una idea que conduce a una soluci´on trivial: llamar tantas veces a la funci´on que
hemos dise˜nado en el apartado anterior como elementos haya originalmente en la lista. Pero,
como comprender´as, se trata de una aproximaci´on muy ineficiente: si la lista tiene n nodos,
llamaremos n veces a una funci´on que, en el peor de los casos, recorre la lista completa, es
decir, da n ((pasos)) para completarse. Es m´as eficiente borrar todos los elementos de una sola
pasada, en tiempo directamente proporcional a n.
Sup´on que recibimos esta lista:
lista 3
info sig
8
info sig
2
info sig
8
info sig
1
info sig
y nos piden eliminar todos los nodos cuyo campo info vale 8.
Nuestro problema es localizar el primer 8 y borrarlo dejando los dos punteros auxiliares en
un estado tal que podamos seguir iterando para encontrar y borrar el siguiente 8 en la lista (y
as´ı con todos los que haya). Ya sabemos c´omo localizar el primer 8. Si usamos un bucle con dos
punteros (aux y atras), llegamos a esta situaci´on:
282 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
atras aux
lista 3
info sig
8
info sig
2
info sig
8
info sig
1
info sig
Si eliminamos el nodo apuntado por aux, nos interesa que aux pase a apuntar al siguiente,
pero que atras quede apuntando al mismo nodo al que apunta ahora (siempre ha de ir un paso
por detr´as de aux):
atras aux
lista 3
info sig
2
info sig
8
info sig
1
info sig
Bueno. No resultar´a tan sencillo. Deberemos tener en cuenta qu´e ocurre en una situaci´on
especial: el borrado del primer elemento de una lista. Aqu´ı tienes una soluci´on:
lista.c
1 TipoLista borra_valor(TipoLista lista, int valor)
2 {
3 struct Nodo * aux, * atras ;
4
5 atras = NULL;
6 aux = lista;
7 while (aux != NULL) {
8 if (aux->info == valor) {
9 if (atras == NULL)
10 lista = aux->sig;
11 else
12 atras->sig = aux->sig;
13 free(aux);
14 if (atras == NULL)
15 aux = lista;
16 else
17 aux = atras->sig;
18 }
19 else {
20 atras = aux;
21 aux = aux->sig;
22 }
23 }
24 return lista;
25 }
Hemos optado por un bucle while en lugar de un bucle for porque necesitamos un mayor
control de los punteros auxiliares (con el for, en cada iteraci´on avanzamos ambos punteros y
no siempre queremos que avancen).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 260 ¿Funciona borra_valor con listas vac´ıas? ¿Y con listas de un s´olo elemento? ¿Y con una
lista en la que todos los elementos coinciden en valor con el entero que buscamos? Si falla en
alguno de estos casos, corrige la funci´on.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.6.12. Inserci´on en una posici´on dada
Vamos a dise˜nar una funci´on que permite insertar un nodo en una posici´on dada de la lista.
Asumiremos la siguiente numeraci´on de posiciones en una lista:
lista 3
info sig
8
info sig
2
info sig
0 1 2 3
Introducci´on a la Programaci´on con C 283
4.6 Listas con enlace simple
while y for
Hemos dicho que el bucle for no resulta conveniente cuando queremos tener un gran control
sobre los punteros auxiliares. No es cierto. El bucle for de C permite emular a cualquier
bucle while. Aqu´ı tienes una versi´on de borra_valor (eliminaci´on de todos los nodos con
un valor dado) que usa un bucle for:
1 TipoLista borra_valor(TipoLista lista, int valor)
2 {
3 struct Nodo * aux, * atras;
4
5 for (atras = NULL, aux = lista; aux != NULL; ) {
6 if (aux->info == valor) {
7 if (atras == NULL)
8 lista = aux->sig;
9 else
10 atras->sig = aux->sig;
11 free(aux);
12 if (atras == NULL)
13 aux = lista;
14 else
15 aux = atras->sig;
16 }
17 else {
18 atras = aux;
19 aux = aux->sig;
20 }
21 }
22 return lista;
23 }
Observa que en el bucle for hemos dejado en blanco la zona que indica c´omo modificar los
punteros aux y atras. Puede hacerse. De hecho, puedes dejar en blanco cualquiera de los
componentes de un bucle for. Una alternativa a while (1), por ejemplo, es for (;;).
O sea, insertar en la posici´on 0 es insertar una nueva cabeza; en la posici´on 1, un nuevo
segundo nodo, etc. ¿Qu´e pasa si se quiere insertar un nodo en una posici´on mayor que la longitud
de la lista? Lo insertaremos en ´ultima posici´on.
El prototipo de la funci´on ser´a:
lista.h
...
extern TipoLista inserta_en_posicion(TipoLista lista, int pos, int valor);
y aqu´ı tienes su implementaci´on:
lista.c
1 TipoLista inserta_en_posicion(TipoLista lista, int pos, int valor)
2 {
3 struct Nodo * aux, * atras, * nuevo;
4 int i;
5
6 nuevo = malloc(sizeof(struct Nodo));
7 nuevo->info = valor;
8
9 for (i=0, atras=NULL, aux=lista; i < pos && aux != NULL; i++, atras = aux, aux = aux->sig) ;
10 nuevo->sig = aux;
11 if (atras == NULL)
12 lista = nuevo;
13 else
14 atras->sig = nuevo;
15 return lista;
16 }
284 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 261 Modifica la funci´on para que, si nos pasan un n´umero de posici´on mayor que el n´umero
de elementos de la lista, no se realice inserci´on alguna.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.6.13. Inserci´on ordenada
Las listas que hemos manejado hasta el momento est´an desordenadas, es decir, sus nodos est´an
dispuestos en un orden arbitrario. Es posible mantener listas ordenadas si las inserciones se
realizan utilizando siempre una funci´on que respete el orden.
La funci´on que vamos a desarrollar, por ejemplo, inserta un elemento en una lista ordenada
de menor a mayor de modo que la lista resultante sea tambi´en una lista ordenada de menor a
mayor.
lista.c
1 TipoLista inserta_en_orden(TipoLista lista, int valor);
2 {
3 struct Nodo * aux, * atras, * nuevo;
4
5 nuevo = malloc(sizeof(struct Nodo));
6 nuevo->info = valor;
7
8 for (atras = NULL, aux = lista; aux != NULL; atras = aux, aux = aux->sig)
9 if (valor <= aux->info) {
10 /* Aqu´ı insertamos el nodo entre atras y aux. */
11 nuevo->sig = aux;
12 if (atras == NULL)
13 lista = nuevo;
14 else
15 atras->sig = nuevo;
16 /* Y como ya est´a insertado, acabamos. */
17 return lista;
18 }
19 /* Si llegamos aqu´ı, es que nuevo va al final de la lista. */
20 nuevo->sig = NULL;
21 if (atras == NULL)
22 lista = nuevo;
23 else
24 atras->sig = nuevo;
25 return lista;
26 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 262 Haz una traza de la inserci´on del valor 7 con inserta_en_orden en cada una de estas
listas:
a)
lista 1
info sig
3
info sig
8
info sig
b)
lista 12
info sig
15
info sig
23
info sig
c)
lista 1
info sig
7
info sig
9
info sig
Introducci´on a la Programaci´on con C 285
4.6 Listas con enlace simple
d)
lista
e)
lista 1
info sig
f)
lista 10
info sig
· 263 Dise˜na una funci´on de inserci´on ordenada en lista que inserte un nuevo nodo si y s´olo
si no hab´ıa ning´un otro con el mismo valor.
· 264 Determinar la pertenencia de un valor a una lista ordenada no requiere que recorras
siempre toda la lista. Dise˜na una funci´on que determine la pertenencia a una lista ordenada
efectuando el menor n´umero posible de comparaciones y desplazamientos sobre la lista.
· 265 Implementa una funci´on que ordene una lista cualquiera mediante el m´etodo de la
burbuja.
· 266 Dise˜na una funci´on que diga, devolviendo el valor 1 o el valor 0, si una lista est´a
ordenada o desordenada.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.6.14. Concatenaci´on de dos listas
La funci´on que dise˜naremos ahora recibe dos listas y devuelve una nueva lista que resulta de
concatenar (una copia de) ambas.
lista.c
1 TipoLista concatena_listas(TipoLista a, TipoLista b)
2 {
3 TipoLista c = NULL;
4 struct Nodo * aux, * nuevo, * anterior = NULL;
5
6 for (aux = a; aux != NULL; aux = aux->sig) {
7 nuevo = malloc( sizeof(struct Nodo) );
8 nuevo->info = aux->info;
9 if (anterior != NULL)
10 anterior->sig = nuevo;
11 else
12 c = nuevo;
13 anterior = nuevo;
14 }
15 for (aux = b; aux != NULL; aux = aux->sig) {
16 nuevo = malloc( sizeof(struct Nodo) );
17 nuevo->info = aux->info;
18 if (anterior != NULL)
19 anterior->sig = nuevo;
20 else
21 c = nuevo;
22 anterior = nuevo;
23 }
24 if (anterior != NULL)
25 anterior->sig = NULL;
26 return c;
27 }
286 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 267 Dise˜na una funci´on que a˜nada a una lista una copia de otra lista.
· 268 Dise˜na una funci´on que devuelva una lista con los elementos de otra lista que sean
mayores que un valor dado.
· 269 Dise˜na una funci´on que devuelva una lista con los elementos comunes a otras dos listas.
· 270 Dise˜na una funci´on que devuelva una lista que es una copia invertida de otra lista.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.6.15. Borrado de la lista completa
Acabaremos este apartado con una rutina que recibe una lista y borra todos y cada uno de sus
nodos. A estas alturas no deber´ıa resultarte muy dif´ıcil de entender:
lista.c
1 TipoLista libera_lista(TipoLista lista)
2 {
3 struct Nodo *aux, *otroaux;
4
5 aux = lista;
6 while (aux != NULL) {
7 otroaux = aux->sig;
8 free(aux);
9 aux = otroaux;
10 }
11 return NULL;
12 }
Alternativamente podr´ıamos definir la rutina de liberaci´on como un procedimiento:
lista.c
1 void libera_lista(TipoLista * lista)
2 {
3 struct Nodo *aux, *otroaux;
4
5 aux = *lista;
6 while (aux != NULL) {
7 otroaux = aux->sig;
8 free(aux);
9 aux = otroaux;
10 }
11 *lista = NULL;
12 }
De este modo nos aseguramos de que el puntero lista fija su valor a NULL.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 271 Dise˜na una funci´on que devuelva un ((corte)) de la lista. Se proporcionar´an como
par´ametros dos enteros i y j y se devolver´a una lista con una copia de los nodos que ocu-
pan las posiciones i a j − 1, ambas inclu´ıdas.
· 272 Dise˜na una funci´on que elimine un ((corte)) de la lista. Se proporcionar´an como par´ametros
dos enteros i y j y se eliminar´an los nodos que ocupan las posiciones i a j − 1, ambas inclu´ıdas.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.6.16. Juntando las piezas
Te ofrecemos, a modo de resumen, todas las funciones que hemos desarrollado a lo largo de la
secci´on junto con un programa de prueba (faltan, naturalmente, las funciones cuyo desarrollo
se propone como ejercicio).
Introducci´on a la Programaci´on con C 287
4.6 Listas con enlace simple
lista.h lista.h
1 struct Nodo {
2 int info;
3 struct Nodo * sig;
4 };
5
6 typedef struct Nodo * TipoLista;
7
8 extern TipoLista lista_vacia(void);
9 extern int es_lista_vacia(TipoLista lista);
10 extern TipoLista inserta_por_cabeza(TipoLista lista, int valor);
11 extern TipoLista inserta_por_cola(TipoLista lista, int valor);
12 extern TipoLista borra_cabeza(TipoLista lista);
13 extern TipoLista borra_cola(TipoLista lista);
14 extern int longitud_lista(TipoLista lista);
15 extern void muestra_lista(TipoLista lista);
16 extern int pertenece(TipoLista lista, int valor);
17 extern TipoLista borra_primera_ocurrencia(TipoLista lista, int valor);
18 extern TipoLista borra_valor(TipoLista lista, int valor);
19 extern TipoLista inserta_en_posicion(TipoLista lista, int pos, int valor);
20 extern TipoLista inserta_en_orden(TipoLista lista, int valor);
21 extern TipoLista concatena_listas(TipoLista a, TipoLista b);
22 extern TipoLista libera_lista(TipoLista lista);
lista.c lista.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include "lista.h"
4
5 TipoLista lista_vacia(void)
6 {
7 return NULL;
8 }
9
10 int es_lista_vacia(TipoLista lista)
11 {
12 return lista == NULL;
13 }
14
15 TipoLista inserta_por_cabeza(TipoLista lista, int valor)
16 {
17 struct Nodo * nuevo = malloc(sizeof(struct Nodo));
18
19 nuevo->info = valor;
20 nuevo->sig = lista;
21 lista = nuevo;
22 return lista;
23 }
24
25 TipoLista inserta_por_cola(TipoLista lista, int valor)
26 {
27 struct Nodo * aux, * nuevo;
28
29 nuevo = malloc(sizeof(struct Nodo));
30 nuevo->info = valor;
31 nuevo->sig = NULL;
32 if (lista == NULL)
33 lista = nuevo;
34 else {
35 for (aux = lista; aux->sig != NULL; aux = aux->sig) ;
36 aux->sig = nuevo;
37 }
288 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
38 return lista;
39 }
40
41 TipoLista borra_cabeza(TipoLista lista)
42 {
43 struct Nodo * aux;
44
45 if (lista != NULL) {
46 aux = lista->sig;
47 free(lista);
48 lista = aux;
49 }
50 return lista;
51 }
52
53 TipoLista borra_cola(TipoLista lista)
54 {
55 struct Nodo * aux, * atras;
56
57 if (lista != NULL) {
58 for (atras = NULL, aux = lista; aux->sig != NULL; atras = aux, aux = aux->sig) ;
59 free(aux);
60 if (atras == NULL)
61 lista = NULL;
62 else
63 atras->sig = NULL;
64 }
65 return lista;
66 }
67
68 int longitud_lista(TipoLista lista)
69 {
70 struct Nodo * aux;
71 int contador = 0;
72
73 for (aux = lista; aux != NULL; aux = aux->sig)
74 contador++;
75 return contador;
76 }
77
78 void muestra_lista(TipoLista lista)
79 { // Como la soluci´on al ejercicio 254, no como lo vimos en el texto.
80 struct Nodo * aux;
81
82 printf ("->");
83 for (aux = lista; aux != NULL; aux = aux->sig)
84 printf ("[%d]->", aux->info);
85 printf ("|n");
86 }
87
88 int pertenece(TipoLista lista, int valor)
89 {
90 struct Nodo * aux;
91
92 for (aux=lista; aux != NULL; aux = aux->sig)
93 if (aux->info == valor)
94 return 1;
95 return 0;
96 }
97
98 TipoLista borra_primera_ocurrencia(TipoLista lista, int valor)
99 {
100 struct Nodo * aux, * atras;
Introducci´on a la Programaci´on con C 289
4.6 Listas con enlace simple
101
102 for (atras = NULL, aux=lista; aux != NULL; atras = aux, aux = aux->sig)
103 if (aux->info == valor) {
104 if (atras == NULL)
105 lista = aux->sig;
106 else
107 atras->sig = aux->sig;
108 free(aux);
109 return lista;
110 }
111 return lista;
112 }
113
114 TipoLista borra_valor(TipoLista lista, int valor)
115 {
116 struct Nodo * aux, * atras;
117
118 atras = NULL;
119 aux = lista;
120 while (aux != NULL) {
121 if (aux->info == valor) {
122 if (atras == NULL)
123 lista = aux->sig;
124 else
125 atras->sig = aux->sig;
126 free(aux);
127 if (atras == NULL)
128 aux = lista;
129 else
130 aux = atras->sig;
131 }
132 else {
133 atras = aux;
134 aux = aux->sig;
135 }
136 }
137 return lista;
138 }
139
140 TipoLista inserta_en_posicion(TipoLista lista, int pos, int valor)
141 {
142 struct Nodo * aux, * atras, * nuevo;
143 int i;
144
145 nuevo = malloc(sizeof(struct Nodo));
146 nuevo->info = valor;
147
148 for (i=0, atras=NULL, aux=lista; i < pos && aux != NULL; i++, atras = aux, aux = aux->sig) ;
149 nuevo->sig = aux;
150 if (atras == NULL)
151 lista = nuevo;
152 else
153 atras->sig = nuevo;
154 return lista;
155 }
156
157 TipoLista inserta_en_orden(TipoLista lista, int valor)
158 {
159 struct Nodo * aux, * atras, * nuevo;
160
161 nuevo = malloc(sizeof(struct Nodo));
162 nuevo->info = valor;
163
290 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
164 for (atras = NULL, aux = lista; aux != NULL; atras = aux, aux = aux->sig)
165 if (valor <= aux->info) {
166 /* Aqu´ı insertamos el nodo entre atras y aux. */
167 nuevo->sig = aux;
168 if (atras == NULL)
169 lista = nuevo;
170 else
171 atras->sig = nuevo;
172 /* Y como ya est´a insertado, acabamos. */
173 return lista;
174 }
175 /* Si llegamos aqu´ı, es que nuevo va al final de la lista. */
176 nuevo->sig = NULL;
177 if (atras == NULL)
178 lista = nuevo;
179 else
180 atras->sig = nuevo;
181 return lista;
182 }
183
184 TipoLista concatena_listas(TipoLista a, TipoLista b)
185 {
186 TipoLista c = NULL;
187 struct Nodo * aux, * nuevo, * anterior = NULL;
188
189 for (aux = a; aux != NULL; aux = aux->sig) {
190 nuevo = malloc( sizeof(struct Nodo) );
191 nuevo->info = aux->info;
192 if (anterior != NULL)
193 anterior->sig = nuevo;
194 else
195 c = nuevo;
196 anterior = nuevo;
197 }
198 for (aux = b; aux != NULL; aux = aux->sig) {
199 nuevo = malloc( sizeof(struct Nodo) );
200 nuevo->info = aux->info;
201 if (anterior != NULL)
202 anterior->sig = nuevo;
203 else
204 c = nuevo;
205 anterior = nuevo;
206 }
207 if (anterior != NULL)
208 anterior->sig = NULL;
209 return c;
210 }
211
212 TipoLista libera_lista(TipoLista lista)
213 {
214 struct Nodo *aux, *otroaux;
215
216 aux = lista;
217 while (aux != NULL) {
218 otroaux = aux->sig;
219 free(aux);
220 aux = otroaux;
221 }
222 return NULL;
223 }
prueba lista.c prueba lista.c
1 #include <stdio.h>
Introducci´on a la Programaci´on con C 291
4.6 Listas con enlace simple
2
3 #include "lista.h"
4
5 int main(void)
6 {
7 TipoLista l, l2, l3;
8
9 printf ("Creaci´on de listan");
10 l = lista_vacia();
11 muestra_lista(l);
12
13 printf ("
?
Es lista vac´ıa?: %dn", es_lista_vacia(l));
14
15 printf ("Inserci´on por cabeza de 2, 8, 3n");
16 l = inserta_por_cabeza(l, 2);
17 l = inserta_por_cabeza(l, 8);
18 l = inserta_por_cabeza(l, 3);
19 muestra_lista(l);
20
21 printf ("Longitud de la lista: %dn", longitud_lista(l));
22
23 printf ("Inserci´on por cola de 1, 5, 10n");
24 l = inserta_por_cola(l, 1);
25 l = inserta_por_cola(l, 5);
26 l = inserta_por_cola(l, 10);
27 muestra_lista(l);
28
29 printf ("Borrado de cabezan");
30 l = borra_cabeza(l);
31 muestra_lista(l);
32
33 printf ("Borrado de colan");
34 l = borra_cola(l);
35 muestra_lista(l);
36
37 printf ("
?
Pertenece 5 a la lista: %dn", pertenece(l, 5));
38 printf ("
?
Pertenece 7 a la lista: %dn", pertenece(l, 7));
39
40 printf ("Inserci´on por cola de 1n");
41 l = inserta_por_cola(l, 1);
42 muestra_lista(l);
43
44 printf ("Borrado de primera ocurrencia de 1n");
45 l = borra_primera_ocurrencia(l, 1);
46 muestra_lista(l);
47
48 printf ("Nuevo borrado de primera ocurrencia de 1n");
49 l = borra_primera_ocurrencia(l, 1);
50 muestra_lista(l);
51
52 printf ("Nuevo borrado de primera ocurrencia de 1 (que no est´a)n");
53 l = borra_primera_ocurrencia(l, 1);
54 muestra_lista(l);
55
56 printf ("Inserci´on por cola y por cabeza de 2n");
57 l = inserta_por_cola(l, 2);
58 l = inserta_por_cabeza(l, 2);
59 muestra_lista(l);
60
61 printf ("Borrado de todas las ocurrencias de 2n");
62 l = borra_valor(l, 2);
63 muestra_lista(l);
64
292 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
65 printf ("Borrado de todas las ocurrencias de 8n");
66 l = borra_valor(l, 8);
67 muestra_lista(l);
68
69 printf ("Inserci´on de 1 en posici´on 0n");
70 l = inserta_en_posicion(l, 0, 1);
71 muestra_lista(l);
72
73 printf ("Inserci´on de 10 en posici´on 2n");
74 l = inserta_en_posicion(l, 2, 10);
75 muestra_lista(l);
76
77 printf ("Inserci´on de 3 en posici´on 1n");
78 l = inserta_en_posicion(l, 1, 3);
79 muestra_lista(l);
80
81 printf ("Inserci´on de 4, 0, 20 y 5 en ordenn");
82 l = inserta_en_orden(l, 4);
83 l = inserta_en_orden(l, 0);
84 l = inserta_en_orden(l, 20);
85 l = inserta_en_orden(l, 5);
86 muestra_lista(l);
87
88 printf ("Creaci´on de una nueva lista con los elementos 30, 40, 50n");
89 l2 = lista_vacia();
90 l2 = inserta_por_cola(l2, 30);
91 l2 = inserta_por_cola(l2, 40);
92 l2 = inserta_por_cola(l2, 50);
93 muestra_lista(l2);
94
95 printf ("Concatenaci´on de las dos listas para formar una nuevan");
96 l3 = concatena_listas(l, l2);
97 muestra_lista(l3);
98
99 printf ("Liberaci´on de las tres listasn");
100 l = libera_lista(l);
101 l2 = libera_lista(l2);
102 l3 = libera_lista(l3);
103 muestra_lista(l);
104 muestra_lista(l2);
105 muestra_lista(l3);
106
107 return 0;
108 }
Recuerda que debes compilar estos programas en al menos dos pasos:
$ gcc lista.c -c
$ gcc prueba_lista.c lista.o -o prueba_lista
Este es el resultado en pantalla de la ejecuci´on de prueba lista:
Creaci´on de lista
->|
?
Es lista vac´ıa?: 1
Inserci´on por cabeza de 2, 8, 3
->[3]->[8]->[2]->|
Longitud de la lista: 3
Inserci´on por cola de 1, 5, 10
->[3]->[8]->[2]->[1]->[5]->[10]->|
Borrado de cabeza
->[8]->[2]->[1]->[5]->[10]->|
Borrado de cola
->[8]->[2]->[1]->[5]->|
Introducci´on a la Programaci´on con C 293
4.7 Listas simples con punteros a cabeza y cola
?
Pertenece 5 a la lista: 1
?
Pertenece 7 a la lista: 0
Inserci´on por cola de 1
->[8]->[2]->[1]->[5]->[1]->|
Borrado de primera ocurrencia de 1
->[8]->[2]->[5]->[1]->|
Nuevo borrado de primera ocurrencia de 1
->[8]->[2]->[5]->|
Nuevo borrado de primera ocurrencia de 1 (que no est´a)
->[8]->[2]->[5]->|
Inserci´on por cola y por cabeza de 2
->[2]->[8]->[2]->[5]->[2]->|
Borrado de todas las ocurrencias de 2
->[8]->[5]->|
Borrado de todas las ocurrencias de 8
->[5]->|
Inserci´on de 1 en posici´on 0
->[1]->[5]->|
Inserci´on de 10 en posici´on 2
->[1]->[5]->[10]->|
Inserci´on de 3 en posici´on 1
->[1]->[3]->[5]->[10]->|
Inserci´on de 4, 0, 20 y 5 en orden
->[0]->[1]->[3]->[4]->[5]->[5]->[10]->[20]->|
Creaci´on de una nueva lista con los elementos 30, 40, 50
->[30]->[40]->[50]->|
Concatenaci´on de las dos listas para formar una nueva
->[0]->[1]->[3]->[4]->[5]->[5]->[10]->[20]->[30]->[40]->[50]->|
Liberaci´on de las tres listas
->|
->|
->|
4.7. Listas simples con punteros a cabeza y cola
Las listas que hemos estudiado hasta el momento son muy r´apidas para, por ejemplo, la inserci´on
de elementos por la cabeza. Como la cabeza est´a permanentemente apuntada por un puntero,
basta con pedir memoria para un nuevo nodo y hacer un par de ajustes con punteros:
hacer que el nodo que sigue al nuevo nodo sea el que era apuntado por el puntero a cabeza,
y hacer que el puntero a cabeza apunte ahora al nuevo nodo.
No importa cu´an larga sea la lista: la inserci´on por cabeza es siempre igual de r´apida. Requiere
una cantidad de tiempo constante. Pero la inserci´on por cola est´a seriamente penalizada en
comparaci´on con la inserci´on por cabeza. Como no sabemos d´onde est´a el ´ultimo elemento,
hemos de recorrer la lista completa cada vez que deseamos a˜nadir por la cola. Una forma de
eliminar este problema consiste en mantener siempre dos punteros: uno al primer elemento de
la lista y otro al ´ultimo.
La nueva estructura de datos que representa una lista podr´ıa definirse as´ı:
lista cabeza cola.h
1 struct Nodo {
2 int info;
3 struct Nodo * sig;
4 };
5
6 struct Lista_cc {
7 struct Nodo * cabeza;
8 struct Nodo * cola;
9 };
Podemos representar gr´aficamente una lista con punteros a cabeza y cola as´ı:
294 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
lista
cabeza
cola
3
info sig
8
info sig
2
info sig
Los punteros lista.cabeza y lista.cola forman un ´unico objeto del tipo lista_cc.
Vamos a presentar ahora unas funciones que gestionan listas con punteros a cabeza y cola.
Afortunadamente, todo lo aprendido con las listas del apartado anterior nos vale. Eso s´ı, algu-
nas operaciones se simplificar´an notablemente (a˜nadir por la cola, por ejemplo), pero otras se
complicar´an ligeramente (eliminar la cola, por ejemplo), ya que ahora hemos de encargarnos de
mantener siempre un nuevo puntero (lista.cola) apuntando correctamente al ´ultimo elemento
de la lista.
4.7.1. Creaci´on de lista vac´ıa
La funci´on que crea una lista vac´ıa es, nuevamente, muy sencilla. El prototipo es ´este:
lista cabeza cola.h
1 extern struct Lista_cc crea_lista_cc_vacia(void);
y su implementaci´on:
lista cabeza cola.c
1 struct Lista_cc crea_lista_cc_vacia(void)
2 {
3 struct Lista_cc lista;
4 lista.cabeza = lista.cola = NULL;
5 return lista;
6 }
Una lista vac´ıa puede representarse as´ı:
lista
cabeza
cola
4.7.2. Inserci´on de nodo en cabeza
La inserci´on de un nodo en cabeza s´olo requiere, en principio, modificar el valor del campo
cabeza, ¿no? Veamos, si tenemos una lista como ´esta:
lista
cabeza
cola
3
info sig
8
info sig
2
info sig
y deseamos insertar el valor 1 en cabeza, basta con modificar lista.cabeza y ajustar el campo
sig del nuevo nodo para que apunte a la antigua cabeza. Como puedes ver, lista.cola sigue
apuntando al mismo lugar al que apuntaba inicialmente:
lista
cabeza
cola
1
info sig
3
info sig
8
info sig
2
info sig
Ya est´a, ¿no? No. Hay un caso en el que tambi´en hemos de modificar lista.cola adem´as de
lista.cabeza: cuando la lista est´a inicialmente vac´ıa. ¿Por qu´e? Porque el nuevo nodo de la lista
ser´a cabeza y cola a la vez.
F´ıjate, si partimos de esta lista:
lista
cabeza
cola
e insertamos el valor 1, hemos de construir esta otra:
Introducci´on a la Programaci´on con C 295
4.7 Listas simples con punteros a cabeza y cola
lista
cabeza
cola
1
info sig
Si s´olo modific´asemos el valor de lista.cabeza, tendr´ıamos esta otra lista mal formada en la que
lista.cola no apunta al ´ultimo elemento:
lista
cabeza
cola
1
info sig
Ya estamos en condiciones de presentar la funci´on:
lista cabeza cola.c
1 struct Lista_cc inserta_por_cabeza(struct Lista_cc lista, int valor)
2 {
3 struct Nodo * nuevo;
4
5 nuevo = malloc(sizeof(struct Nodo));
6 nuevo->info = valor;
7 nuevo->sig = lista.cabeza;
8 if (lista.cabeza == NULL)
9 lista.cola = nuevo;
10 lista.cabeza = nuevo;
11 return lista;
12 }
4.7.3. Inserci´on de nodo en cola
La inserci´on de un nodo en cola no es mucho m´as complicada. Como sabemos siempre cu´al es
el ´ultimo elemento de la lista, no hemos de buscarlo con un bucle. El procedimiento a seguir es
´este:
1. Pedimos memoria para un nuevo nodo apuntado por un puntero nuevo,
nuevo
info sig
lista
cabeza
cola
3
info sig
8
info sig
2
info sig
2. asignamos un valor a nuevo->info y hacemos que el nuevo->sig sea NULL
nuevo 1
info sig
lista
cabeza
cola
3
info sig
8
info sig
2
info sig
3. hacemos que lista.cola->sig apunte a nuevo,
nuevo 1
info sig
lista
cabeza
cola
3
info sig
8
info sig
2
info sig
4. y actualizamos lista.cola para que pase a apuntar a nuevo.
296 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
nuevo 1
info sig
lista
cabeza
cola
3
info sig
8
info sig
2
info sig
Reordenando el gr´afico tenemos:
nuevo
lista
cabeza
cola
3
info sig
8
info sig
2
info sig
1
info sig
La ´unica precauci´on que hemos de tener es que, cuando la lista est´e inicialmente vac´ıa, se
modifique tanto el puntero a la cabeza como el puntero a la cola para que ambos apunten a
nuevo.
lista cabeza cola.c
1 struct Lista_cc inserta_por_cola(struct Lista_cc lista, int valor)
2 {
3 struct Nodo * nuevo;
4
5 nuevo = malloc(sizeof(struct Nodo));
6 nuevo->info = valor;
7 nuevo->sig = NULL;
8
9 if (lista.cola != NULL) {
10 lista.cola->sig = nuevo;
11 lista.cola = nuevo;
12 }
13 else
14 lista.cabeza = lista.cola = nuevo;
15 return lista;
16 }
F´ıjate: la inserci´on por cola en este tipo de listas es tan eficiente como la inserci´on por
cabeza. No importa lo larga que sea la lista: siempre cuesta lo mismo insertar por cola, una
cantidad constante de tiempo. Acaba de rendir su primer fruto el contar con punteros a cabeza
y cola.
4.7.4. Borrado de la cabeza
Eliminar un elemento de la cabeza ha de resultar sencillo con la experiencia adquirida:
1. Si la lista est´a vac´ıa, no hacemos nada.
2. Si la lista tiene un s´olo elemento, lo eliminamos y ponemos lista.cabeza y lista.cola a NULL.
3. Y si la lista tiene m´as de un elemento, como ´esta:
lista
cabeza
cola
3
info sig
8
info sig
2
info sig
seguimos este proceso:
a) Mantenemos un puntero auxiliar apuntando a la actual cabeza,
aux
lista
cabeza
cola
3
info sig
8
info sig
2
info sig
Introducci´on a la Programaci´on con C 297
4.7 Listas simples con punteros a cabeza y cola
b) hacemos que lista.cabeza apunte al sucesor de la cabeza actual,
aux
lista
cabeza
cola
3
info sig
8
info sig
2
info sig
c) y liberamos la memoria ocupada por el primer nodo.
aux
lista
cabeza
cola
8
info sig
2
info sig
lista cabeza cola.c
1 struct Lista_cc borra_cabeza(struct Lista_cc lista)
2 {
3 struct Nodo * aux;
4
5 /* Lista vac´ıa: nada que borrar. */
6 if (lista.cabeza == NULL)
7 return lista;
8
9 /* Lista con un solo nodo: se borra el nodo y la cabeza y la cola pasan a ser NULL. */
10 if (lista.cabeza == lista.cola) {
11 free(lista.cabeza);
12 lista.cabeza = lista.cola = NULL;
13 return lista;
14 }
15
16 /* Lista con m´as de un elemento. */
17 aux = lista.cabeza;
18 lista.cabeza = aux->sig;
19 free(aux);
20 return lista;
21 }
4.7.5. Borrado de la cola
El borrado del ´ultimo elemento de una lista con punteros a cabeza y cola plantea un pro-
blema: cuando hayamos eliminado el nodo apuntado por lista.cola, ¿a qui´en debe apuntar
lista.cola? Naturalmente, al que hasta ahora era el pen´ultimo nodo. ¿Y c´omo sabemos cu´al era
el pen´ultimo? S´olo hay una forma de saberlo: busc´andolo con un recorrido de los nodos de la
lista.
Nuevamente distinguiremos tres casos distintos en funci´on de la talla de la lista:
1. Si la lista est´a vac´ıa, no hacemos nada.
2. Si la lista tiene un ´unico elemento, liberamos su memoria y hacemos que los punteros a
cabeza y cola apunten a NULL.
3. En otro caso, actuaremos como en este ejemplo,
lista
cabeza
cola
3
info sig
8
info sig
2
info sig
a) buscamos el pen´ultimo elemento (sabremos cu´al es porque si se le apunta con aux,
entonces aux->sig coincide con lista.cola) y lo apuntamos con una variable auxiliar
aux,
298 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
aux
lista
cabeza
cola
3
info sig
8
info sig
2
info sig
b) hacemos que el pen´ultimo no tenga siguiente nodo (ponemos su campo sig a NULL)
para que as´ı pase a ser el ´ultimo,
aux
lista
cabeza
cola
3
info sig
8
info sig
2
info sig
c) liberamos la memoria del que hasta ahora era el ´ultimo nodo (el apuntado por
lista.cola)
aux
lista
cabeza
cola
3
info sig
8
info sig
d) y, finalmente, hacemos que lista.cola apunte a aux.
aux
lista
cabeza
cola
3
info sig
8
info sig
lista cabeza cola.c
1 struct Lista_cc borra_cola(struct Lista_cc lista)
2 {
3 struct Nodo * aux;
4
5 /* Lista vac´ıa. */
6 if (lista.cabeza == NULL)
7 return lista;
8
9 /* Lista con un solo nodo. */
10 if (lista.cabeza == lista.cola) {
11 free(lista.cabeza);
12 lista.cabeza = lista.cola = NULL;
13 return lista;
14 }
15
16 /* Lista con m´as de un nodo. */
17 for (aux = lista.cabeza; aux->sig != lista.cola ; aux = aux->sig) ;
18 aux->sig = NULL;
19 free(lista.cola);
20 lista.cola = aux;
21 return lista;
22 }
F´ıjate en la condici´on del bucle: detecta si hemos llegado o no al pen´ultimo nodo preguntando
si el que sigue a aux es el ´ultimo (el apuntado por lista.cola).
La operaci´on de borrado de la cola no es, pues, tan eficiente como la de borrado de la cabeza,
pese a que tenemos un puntero a la cola. El tiempo que necesita es directamente proporcional
a la longitud de la lista.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 273 Dise˜na una funci´on que determine si un n´umero pertenece o no a una lista con punteros
a cabeza y cola.
Introducci´on a la Programaci´on con C 299
4.8 Listas con enlace doble
· 274 Dise˜na una funci´on que elimine el primer nodo con un valor dado en una lista con
punteros a cabeza y cola.
· 275 Dise˜na una funci´on que elimine todos los nodos con un valor dado en una lista con
punteros a cabeza y cola.
· 276 Dise˜na una funci´on que devuelva el elemento que ocupa la posici´on n en una lista con
puntero a cabeza y cola. (La cabeza ocupa la posici´on 0.) La funci´on devolver´a como valor de
retorno 1 o 0 para, respectivamente, indicar si la operaci´on se pudo completar con ´exito o si
fracas´o. La operaci´on no se puede completar con ´exito si n es negativo o si n es mayor o igual
que la talla de la lista. El valor del nodo se devolver´a en un par´ametro pasado por referencia.
· 277 Dise˜na una funci´on que devuelva un ((corte)) de la lista. Se recibir´an dos ´ındices i y j y
se devolver´a una nueva lista con punteros a cabeza y cola con una copia de los nodos que van
del que ocupa la posici´on i al que ocupa la posici´on j − 1, ambos inclu´ıdos. La lista devuelta
tendr´a punteros a cabeza y cola.
· 278 Dise˜na una funci´on de inserci´on ordenada en una lista ordenada con punteros a cabeza
y cola.
· 279 Dise˜na una funci´on que devuelva el menor valor de una lista ordenada con punteros a
cabeza y cola.
· 280 Dise˜na una funci´on que devuelva el mayor valor de una lista ordenada con punteros a
cabeza y cola.
· 281 Dise˜na una funci´on que a˜nada a una lista con punteros a cabeza y cola una copia de
otra lista con punteros a cabeza y cola.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.8. Listas con enlace doble
Vamos a dotar a cada nodo de dos punteros: uno al siguiente nodo en la lista y otro al anterior.
Los nodos ser´an variables de este tipo:
lista doble.h
1 struct DNodo {
2 int info; // Valor del nodo.
3 struct DNodo * ant; // Puntero al anterior.
4 struct DNodo * sig; // Puntero al siguiente.
5 };
Una lista es un puntero a un struct DNodo (o a NULL). Nuevamente, definiremos un tipo para
poner ´enfasis en que un puntero representa a la lista que ((cuelga)) de ´el.
1 typedef struct DNodo * TipoDLista;
Aqu´ı tienes una representaci´on gr´afica de una lista doblemente enlazada:
lista 3
ant info sig
8
ant info sig
2
ant info sig
Observa que cada nodo tiene dos punteros: uno al nodo anterior y otro al siguiente. ¿Qu´e nodo
sigue al ´ultimo nodo? Ninguno, o sea, NULL. ¿Y cu´al antecede al primero? Ninguno, es decir,
NULL.
4.8.1. Inserci´on por cabeza
La inserci´on por cabeza es relativamente sencilla. Tratemos en primer lugar el caso general: la
inserci´on por cabeza en una lista no vac´ıa. Por ejemplo, en ´esta:
lista 8
ant info sig
2
ant info sig
Vamos paso a paso.
300 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
1. Empezamos pidiendo memoria para un nuevo nodo:
nuevo
ant info sig
lista 8
ant info sig
2
ant info sig
2. Asignamos el valor que nos indiquen al campo info:
nuevo 3
ant info sig
lista 8
ant info sig
2
ant info sig
3. Ajustamos sus punteros ant y sig:
nuevo 3
ant info sig
lista 8
ant info sig
2
ant info sig
4. Ajustamos el puntero ant del que hasta ahora ocupaba la cabeza:
nuevo 3
ant info sig
lista 8
ant info sig
2
ant info sig
5. Y, finalmente, hacemos que lista apunte al nuevo nodo:
nuevo
lista 3
ant info sig
8
ant info sig
2
ant info sig
El caso de la inserci´on en la lista vac´ıa es trivial: se pide memoria para un nuevo nodo cuyos
punteros ant y sig se ponen a NULL y hacemos que la cabeza apunte a dicho nodo.
Aqu´ı tienes la funci´on que codifica el m´etodo descrito. Hemos factorizado y dispuesto al
principio los elementos comunes al caso general y al de la lista vac´ıa:
lista doble.c
1 TipoDLista inserta_por_cabeza(TipoDLista lista, int valor)
2 {
3 struct DNodo * nuevo;
4
5 nuevo = malloc(sizeof(struct DNodo));
6 nuevo->info = valor;
7 nuevo->ant = NULL;
8 nuevo->sig = lista;
9
10 if (lista != NULL)
11 lista->ant = nuevo;
12
13 lista = nuevo;
14 return lista;
15 }
Introducci´on a la Programaci´on con C 301
4.8 Listas con enlace doble
Te proponemos como ejercicios algunas de las funciones b´asicas para el manejo de listas
doblemente enlazadas:
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 282 Dise˜na una funci´on que inserte un nuevo nodo al final de una lista doblemente enlazada.
· 283 Dise˜na una funci´on que borre la cabeza de una lista doblemente enlazada. Presta
especial atenci´on al caso en el que la lista consta de un s´olo elemento.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.8.2. Borrado de la cola
Vamos a desarrollar la funci´on de borrado del ´ultimo elemento de una lista doblemente enlazada,
pues presenta alg´un aspecto interesante.
Desarrollemos nuevamente el caso general sobre una lista concreta para deducir el m´etodo
a seguir. Tomemos, por ejemplo, ´esta:
lista 3
ant info sig
8
ant info sig
2
ant info sig
1. Empezamos localizando el ´ultimo elemento de la lista (con un bucle) y apunt´andolo con
un puntero:
lista 3
ant info sig
8
ant info sig
aux
2
ant info sig
2. Y localizamos ahora el pen´ultimo en un s´olo paso (es aux->ant):
lista 3
ant info sig
atras
8
ant info sig
aux
2
ant info sig
3. Se elimina el ´ultimo nodo (el apuntado por aux):
lista 3
ant info sig
atras
8
ant info sig
aux
4. Y se pone el campo sig del que hasta ahora era pen´ultimo (el apuntado por atras) a NULL.
lista 3
ant info sig
atras
8
ant info sig
aux
El caso de la lista vac´ıa tiene f´acil soluci´on: no hay nada que borrar. Es m´as problem´atica la
lista con s´olo un nodo. El problema con ella estriba en que no hay elemento pen´ultimo (el anterior
al ´ultimo es NULL). Tendremos, pues, que detectar esta situaci´on y tratarla adecuadamente.
lista doble.c
1 TipoDLista borra_por_cola(TipoDLista lista)
2 {
3 struct DNodo * aux, * atras;
4
5 /* Lista vac´ıa. */
6 if (lista == NULL)
302 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
7 return lista;
8
9 /* Lista con un nodo. */
10 if (lista->sig == NULL) {
11 free(lista);
12 lista = NULL;
13 return lista;
14 }
15
16 /* Caso general. */
17 for (aux=lista; aux->sig!=NULL; aux=aux->sig) ;
18 atras = aux->ant;
19 free(aux);
20 atras->sig = NULL;
21 return lista;
22 }
4.8.3. Inserci´on en una posici´on determinada
Tratemos ahora el caso de la inserci´on de un nuevo nodo en la posici´on n de una lista doblemente
enlazada.
lista 3
ant info sig
8
ant info sig
2
ant info sig
0 1 2 3
Si n est´a fuera del rango de ´ındices ((v´alidos)), insertaremos en la cabeza (si n es negativo) o en
la cola (si n es mayor que el n´umero de elementos de la lista).
A simple vista percibimos ya diferentes casos que requerir´an estrategias diferentes:
La lista vac´ıa: la soluci´on en este caso es trivial.
Inserci´on al principio de la lista: seguiremos la misma rutina dise˜nada para insertar por
cabeza y, por qu´e no, utilizaremos la funci´on que dise˜namos en su momento.
Inserci´on al final de la lista: ´ıdem.7
Inserci´on entre dos nodos de una lista.
Vamos a desarrollar completamente el ´ultimo caso. Nuevamente usaremos una lista concreta
para deducir cada uno de los detalles del m´etodo. Insertaremos el valor 1 en la posici´on 2 de
esta lista:
lista 3
ant info sig
8
ant info sig
2
ant info sig
1. Empezamos localizando el elemento que ocupa actualmente la posici´on n. Un simple bucle
efectuar´a esta labor:
lista 3
ant info sig
8
ant info sig
aux
2
ant info sig
2. Pedimos memoria para un nuevo nodo, lo apuntamos con el puntero nuevo y le asignamos
el valor:
lista 3
ant info sig
8
ant info sig
aux
2
ant info sig
nuevo 1
ant info sig
7Ver m´as adelante el ejercicio 285.
Introducci´on a la Programaci´on con C 303
4.8 Listas con enlace doble
3. Hacemos que nuevo->sig sea aux:
lista 3
ant info sig
8
ant info sig
aux
2
ant info sig
nuevo 1
ant info sig
4. Hacemos que nuevo->ant sea aux->ant:
lista 3
ant info sig
8
ant info sig
aux
2
ant info sig
nuevo 1
ant info sig
5. Ojo con este paso, que es complicado. Hacemos que el anterior a aux tenga como siguiente
a nuevo, es decir, aux->ant->sig = nuevo:
lista 3
ant info sig
8
ant info sig
aux
2
ant info sig
nuevo 1
ant info sig
6. Y ya s´olo resta que el anterior a aux sea nuevo con la asignaci´on aux->ant = nuevo:
lista 3
ant info sig
8
ant info sig
aux
2
ant info sig
nuevo 1
ant info sig
Ahora que tenemos claro el procedimiento, podemos escribir la funci´on:
lista doble.c
1 TipoDLista inserta_en_posicion(TipoDLista lista, int pos, int valor)
2 {
3 struct DNodo * aux, * nuevo;
4 int i;
5
6 /* Caso especial: lista vac´ıa */
7 if (lista == NULL) {
8 lista = inserta_por_cabeza(lista, valor);
9 return lista;
10 }
11
12 /* Inserci´on en cabeza en lista no vac´ıa. */
13 if (pos <= 0) {
14 lista = inserta_por_cabeza(lista, valor);
15 return lista;
16 }
17
304 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
18 /* Inserci´on no en cabeza. */
19 nuevo = malloc(sizeof(struct DNodo));
20 nuevo->info = valor;
21 for (i = 0, aux = lista; i < pos && aux != NULL; i++, aux = aux->sig) ;
22 if (aux == NULL) /* Inserci´on por cola. */
23 lista = inserta_por_cola(lista, valor);
24 else {
25 nuevo->sig = aux;
26 nuevo->ant = aux->ant;
27 aux->ant->sig = nuevo;
28 aux->ant = nuevo;
29 }
30 return lista;
31 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 284 Reescribe la funci´on de inserci´on en una posici´on dada para que no efect´ue llamadas a
la funci´on inserta_por_cabeza.
· 285 Reescribe la funci´on de inserci´on en una posici´on dada para que no efect´ue llamadas a
la funci´on inserta_por_cola. ¿Es m´as eficiente la nueva versi´on? ¿Por qu´e?
· 286 ¿Qu´e ocurrir´ıa si las ´ultimas l´ıneas de la funci´on fueran ´estas?:
1 ...
2 nuevo->sig = aux;
3 nuevo->ant = aux->ant;
4 aux->ant = nuevo;
5 aux->ant->sig = nuevo;
6 }
7 return lista;
8 }
¿Es correcta ahora la funci´on? Haz una traza con un caso concreto.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.8.4. Borrado de la primera aparici´on de un elemento
Nuevamente hay un par de casos triviales: si la lista est´a vac´ıa, no hay que hacer nada y si la
lista tiene un s´olo elemento, s´olo hemos de actuar si ese elemento tiene el valor buscado, en cuyo
caso liberaremos la memoria del nodo en cuesti´on y convertiremos la lista en una lista vac´ıa.
Desarrollemos un caso general. Supongamos que en esta lista hemos de eliminar el primer y
´unico nodo con valor 8:
lista 3
ant info sig
8
ant info sig
2
ant info sig
Vamos paso a paso:
1. Empezamos por localizar el elemento y apuntarlo con un puntero auxiliar aux:
lista 3
ant info sig
aux
8
ant info sig
2
ant info sig
2. Hacemos que el que sigue al anterior de aux sea el siguiente de aux (¡qu´e galimat´ıas!). O
sea, hacemos aux->ant->sig=aux->sig:
lista 3
ant info sig
aux
8
ant info sig
2
ant info sig
Introducci´on a la Programaci´on con C 305
4.8 Listas con enlace doble
3. Ahora hacemos que el que antecede al siguiente de aux sea el anterior a aux. Es decir,
aux->sig->ant=aux->ant:
lista 3
ant info sig
aux
8
ant info sig
2
ant info sig
4. Y ya podemos liberar la memoria ocupada por el nodo apuntado con aux:
lista 3
ant info sig
aux
2
ant info sig
Hemos de ser cautos. Hay un par de casos especiales que merecen ser tratados aparte: el
borrado del primer nodo y el borrado del ´ultimo nodo. Veamos c´omo proceder en el primer
caso: tratemos de borrar el nodo de valor 3 en la lista del ejemplo anterior.
1. Una vez apuntado el nodo por aux, sabemos que es el primero porque apunta al mismo
nodo que lista:
aux
lista 3
ant info sig
8
ant info sig
2
ant info sig
2. Hacemos que el segundo nodo deje de tener antecesor, es decir, que el puntero aux->sig->ant
valga NULL (que, por otra parte, es lo mismo que hacer aux->sig->ant=aux->ant):
aux
lista 3
ant info sig
8
ant info sig
2
ant info sig
3. Ahora hacemos que lista pase a apuntar al segundo nodo (lista=aux->sig):
aux
lista 3
ant info sig
8
ant info sig
2
ant info sig
4. Y por fin, podemos liberar al nodo apuntado por aux (free(aux)):
aux
lista 8
ant info sig
2
ant info sig
Vamos a por el caso en que borramos el ´ultimo elemento de la lista:
1. Empezamos por localizarlo con aux y detectamos que efectivamente es el ´ultimo porque
aux->sig es NULL:
306 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
aux
lista 3
ant info sig
8
ant info sig
2
ant info sig
2. Hacemos que el siguiente del que antecede a aux sea NULL: (aux->ant->sig=NULL):
aux
lista 3
ant info sig
8
ant info sig
2
ant info sig
3. Y liberamos el nodo apuntado por aux:
aux
lista 3
ant info sig
8
ant info sig
lista doble.c
1 TipoDLista borra_primera_ocurrencia(TipoDLista lista, int valor)
2 {
3 struct DNodo * aux;
4
5 for (aux=lista; aux!=NULL; aux=aux->sig)
6 if (aux->info == valor)
7 break;
8
9 if (aux == NULL) // No se encontr´o.
10 return lista;
11
12 if (aux->ant == NULL) // Es el primero de la lista.
13 lista = aux->sig;
14 else
15 aux->ant->sig = aux->sig;
16
17 if (aux->sig != NULL) // No es el ´ultimo de la lista.
18 aux->sig->ant = aux->ant;
19
20 free(aux);
21
22 return lista;
23 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 287 Dise˜na una funci´on que permita efectuar la inserci´on ordenada de un elemento en una
lista con enlace doble que est´a ordenada.
· 288 Dise˜na una funci´on que permita concatenar dos listas doblemente enlazadas. La funci´on
recibir´a las dos listas y devolver´a una lista nueva con una copia de la primera seguida de una
copia de la segunda.
· 289 Dise˜na una funci´on que devuelva una copia invertida de una lista doblemente enlazada.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.9. Listas con enlace doble y puntero a cabeza y cola
Ya sabemos manejar listas con puntero a cabeza y listas con punteros a cabeza y cola. Hemos
visto que las listas con puntero a cabeza son ineficientes a la hora de a˜nadir elementos por
la cola: se tarda tanto m´as cuanto mayor es el n´umero de elementos de la lista. Las listas
Introducci´on a la Programaci´on con C 307
4.9 Listas con enlace doble y puntero a cabeza y cola
con puntero a cabeza y cola permiten realizar operaciones de inserci´on por cola en un n´umero
constante de pasos. A´un as´ı, hay operaciones de cola que tambi´en son ineficientes en esta ´ultima
estructura de datos: la eliminaci´on del nodo de cola, por ejemplo, sigue necesitando un tiempo
proporcional a la longitud de la lista.
La estructura que presentamos en esta secci´on, la lista doblemente enlazada con puntero
a cabeza y cola, corrige la ineficiencia en el borrado del nodo de cola. Una lista doblemente
enlazada con puntero a cabeza y cola puede representarse gr´aficamente as´ı:
lista
cabeza
cola
3
ant info sig
8
ant info sig
2
ant info sig
La definici´on del tipo es f´acil ahora que ya hemos estudiado diferentes tipos de listas:
lista doble cc.h
1 struct DNodo {
2 int info;
3 struct DNodo * ant;
4 struct DNodo * sig;
5 };
6
7 struct DLista_cc {
8 struct DNodo * cabeza;
9 struct DNodo * cola;
10 }
11
12 typedef struct DLista_cc TipoDListaCC;
S´olo vamos a presentarte una de las operaciones sobre este tipo de listas: el borrado de la
cola. El resto de operaciones te las proponemos como ejercicios.
Con cualquiera de las otras estructuras de datos basadas en registros enlazados, el borrado
del nodo de cola no pod´ıa efectuarse en tiempo constante. ´Esta lo hace posible. ¿C´omo? Lo
mejor es que, una vez m´as, despleguemos los diferentes casos y estudiemos ejemplos concretos
cuando convenga:
Si la lista est´a vac´ıa, no hay que hacer nada.
Si la lista tiene un solo elemento, liberamos su memoria y ponemos los punteros a cabeza
y cola a NULL.
Y si la lista tiene m´as de un elemento, como ´esta:
lista
cabeza
cola
3
ant info sig
8
ant info sig
2
ant info sig
hacemos lo siguiente:
a) localizamos al pen´ultimo elemento, que es lista.cola->ant, y lo mantenemos apuntado
con un puntero auxiliar aux:
aux
lista
cabeza
cola
3
ant info sig
8
ant info sig
2
ant info sig
b) liberamos la memoria apuntada por lista.cola:
aux
lista
cabeza
cola
3
ant info sig
8
ant info sig
308 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
c) ponemos aux->sig a NULL:
aux
lista
cabeza
cola
3
ant info sig
8
ant info sig
d) y ajustamos lista.cola para que apunte ahora donde apunta aux:
aux
lista
cabeza
cola
3
ant info sig
8
ant info sig
Ya podemos escribir el programa:
lista doble cc.c
1 TipoDListaCC borra_cola(TipoDListaCC lista)
2 {
3 if (lista.cabeza == NULL)
4 return lista;
5
6 if (lista.cabeza == lista.cola) {
7 free(lista.cabeza);
8 lista.cabeza = lista.cola = NULL;
9 return lista;
10 }
11
12 aux = lista.cola->ant;
13 free(lista.cola);
14 aux->sig = NULL;
15 lista.cola = aux;
16 return lista;
17 }
Ha sido f´acil, ¿no? No ha hecho falta bucle alguno. La operaci´on se ejecuta en un n´umero de
pasos que es independiente de lo larga que sea la lista.
Ahora te toca a t´ı desarrollar c´odigo. Practica con estos ejercicios:
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 290 Dise˜na una funci´on que calcule la longitud de una lista doblemente enlazada con pun-
teros a cabeza y cola.
· 291 Dise˜na una funci´on que permita insertar un nuevo nodo en cabeza.
· 292 Dise˜na una funci´on que permita insertar un nuevo nodo en cola.
· 293 Dise˜na una funci´on que permita borrar el nodo de cabeza.
· 294 Dise˜na una funci´on que elimine el primer elemento de la lista con un valor dado.
· 295 Dise˜na una funci´on que elimine todos los elementos de la lista con un valor dado.
· 296 Dise˜na una funci´on que inserte un nodo en una posici´on determinada que se indica por
su ´ındice.
· 297 Dise˜na una funci´on que inserte ordenadamente en una lista ordenada.
· 298 Dise˜na una funci´on que muestre por pantalla el contenido de una lista, mostrando el
valor de cada celda en una l´ınea. Los elementos se mostrar´an en el mismo orden con el que
aparecen en la lista.
· 299 Dise˜na una funci´on que muestre por pantalla el contenido de una lista, mostrando el
valor de cada celda en un l´ınea. Los elementos se mostrar´an en orden inverso.
· 300 Dise˜na una funci´on que devuelva una copia invertida de una lista doblemente enlazada
con puntero a cabeza y cola.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Introducci´on a la Programaci´on con C 309
4.10 Una gu´ıa para elegir listas
4.10. Una gu´ıa para elegir listas
Hemos estudiado cuatro tipos diferentes de listas basadas en registros enlazados. ¿Por qu´e tan-
tas? Porque cada una supone una soluci´on de compromiso diferente entre velocidad y consumo
de memoria.
Empecemos por estudiar el consumo de memoria. Supongamos que una variable del tipo
del campo info ocupa m bytes, que cada puntero ocupa 4 bytes y que la lista consta de n
elementos. Esta tabla muestra la ocupaci´on en bytes seg´un la estructura de datos escogida:
lista con enlace simple (((simple))), lista con enlace simple y puntero a cabeza y cola (((simple
cabeza/cola))), lista con enlace doble (((doble))), lista con enlace doble y puntero a cabeza y cola
(((doble cabeza/cola))).
memoria (bytes)
simple 4 + n · (4 + m)
simple cabeza/cola 8 + n · (4 + m)
doble 4 + n · (8 + m)
doble cabeza/cola 8 + n · (8 + m)
Esta otra tabla resume el tiempo que requieren algunas operaciones sobre los cuatro tipos
de lista:
simple simple cabeza/cola doble doble cabeza/cola
Insertar por cabeza constante constante constante constante
Borrar cabeza constante constante constante constante
Insertar por cola lineal constante lineal constante
Borrar cola lineal lineal lineal constante
Buscar un nodo concreto lineal∗
lineal∗
lineal∗
lineal∗
Invertir la lista cuadr´atico cuadr´atico lineal lineal
Hemos indicado con la palabra ((constante)) que se requiere una cantidad de tiempo fija,
independiente de la longitud de la lista; con la palabra ((lineal)), que se requiere un tiempo que
es proporcional a la longitud de la lista; y con ((cuadr´atico)), que el coste crece con el cuadrado
del n´umero de elementos.
Para que te hagas una idea: insertar por cabeza un nodo en una lista cuesta siempre la misma
cantidad de tiempo, tenga la lista 100 o 1000 nodos. Insertar por la cola en una lista simplemente
enlazada con puntero a cabeza, sin embargo, es unas 10 veces m´as lento si la lista es 10 veces m´as
larga. Esto no ocurre con una lista simplemente enlazada que tenga puntero a cabeza y cola:
insertar por la cola en ella siempre cuesta lo mismo. ¡Ojo con los costes cuadr´aticos! Invertir
una lista simplemente enlazada de 1000 elementos es 100 veces m´as costoso que invertir una
lista con 10 veces menos elementos.
En la tabla hemos marcado algunos costes con un asterisco. Son costes para el peor de los
casos. Buscar un nodo concreto en una lista obliga a recorrer todos los nodos s´olo si el que
buscamos no est´a o si ocupa la ´ultima posici´on. En el mejor de los casos, el coste temporal
es constante: ello ocurre cuando el nodo buscado se encuentra en la lista y, adem´as, ocupa la
primera posici´on. De los an´alisis de coste nos ocuparemos m´as adelante.
Un an´alisis de la tabla de tiempos permite concluir que la lista doblemente enlazada con
punteros a cabeza y cola es siempre igual o mejor que las otras estructuras. ¿Debemos escogerla
siempre? No, por tres razones:
1. Aunque la lista m´as compleja requiere tiempo constante en muchas operaciones, ´estas
son algo m´as lentas y sofisticadas que operaciones an´alogas en las otras estructuras m´as
sencillas. Son, por fuerza, algo m´as lentas.
2. El consumo de memoria es mayor en la lista m´as compleja (8 bytes adicionales para
cada nodo y 8 bytes para los punteros a cabeza y cola, frente a 4 bytes adicionales para
cada nodo y 4 bytes para un puntero a cabeza en la estructura m´as sencilla), as´ı que
puede no compensar la ganancia en velocidad o, sencillamente, es posible que no podamos
permitirnos el lujo de gastar el doble de memoria extra.
3. Puede que nuestra aplicaci´on s´olo efect´ue operaciones ((baratas)) sobre cualquier lista. Ima-
gina que necesitas una lista en la que siempre insertas y eliminas nodos por cabeza, jam´as
por el final. Las cuatro estructuras ofrecen tiempo constante para esas dos operaciones,
s´olo que, adem´as, las dos primeras son mucho m´as sencillas y consumen menos memoria
que las dos ´ultimas.
310 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 301 Rellena una tabla similar a la anterior para estas otras operaciones:
a) Insertar ordenadamente en una lista ordenada.
b) Insertar en una posici´on concreta.
c) Buscar un elemento en una lista ordenada.
d) Buscar el elemento de valor m´ınimo en una lista ordenada.
e) Buscar el elemento de valor m´aximo en una lista ordenada.
f) Unir dos listas ordenadas de modo que el resultado est´e ordenado.
g) Mostrar el contenido de una lista por pantalla.
h) Mostrar el contenido de una lista en orden inverso por pantalla.
· 302 Vamos a montar una pila con listas. La pila es una estructura de datos en la que s´olo
podemos efectuar las siguientes operaciones:
insertar un elemento en la cima,
eliminar el elemento de la cima,
consultar el valor del elemento de la cima.
¿Qu´e tipo de lista te parece m´as adecuado para implementar una pila? ¿Por qu´e?
· 303 Vamos a montar una cola con listas. La cola es una estructura de datos en la que s´olo
podemos efectuar las siguientes operaciones:
insertar un elemento al final de la cola,
eliminar el elemento que hay al principio de la cola,
consultar el valor del elemento que hay al principio de la cola.
¿Qu´e tipo de lista te parece m´as adecuado para construir una cola? ¿Por qu´e?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.11. Una aplicaci´on: una base de datos para discos compactos
En este apartado vamos a desarrollar una aplicaci´on pr´actica que usa listas: un programa para
la gesti´on de una colecci´on de discos compactos. Cada disco compacto contendr´a un t´ıtulo, un
int´erprete, un a˜no de edici´on y una lista de canciones. De cada canci´on nos interesar´a ´unicamente
el t´ıtulo.
Las acciones del programa, que se presentar´an al usuario con un men´u, son ´estas.
1. A˜nadir un disco.
2. Buscar discos por t´ıtulo.
3. Buscar discos por int´erprete.
4. Buscar discos por t´ıtulo de canci´on.
5. Mostrar el contenido completo de la colecci´on.
6. Eliminar un disco de la base de datos dados su t´ıtulo y el nombre del int´erprete.
7. Finalizar.
Introducci´on a la Programaci´on con C 311
4.11 Una aplicaci´on: una base de datos para discos compactos
A priori no sabemos cu´antas canciones hay en un disco, ni cu´antos discos hay que almacenar
en la base de datos, as´ı que utilizaremos listas para ambas entidades. Nuestra colecci´on ser´a,
pues, una lista de discos que, a su vez, contienen listas de canciones. No s´olo eso: no queremos
que nuestra aplicaci´on desperdicie memoria con cadenas que consumen m´as memoria que la
necesaria, as´ı que usaremos memoria din´amica tambi´en para la reserva de memoria para cadenas.
Lo mejor es dividir el problema en estructuras de datos claramente diferenciadas (una para
la lista de discos y otra para la lista de canciones) y dise˜nar funciones para manejar cada una de
ellas. Atenci´on al montaje que vamos a presentar, pues es el m´as complicado de cuantos hemos
estudiado.
1 struct Cancion {
2 char * titulo;
3 struct Cancion * sig;
4 };
5
6 typedef struct Cancion * TipoListaCanciones;
7
8 struct Disco {
9 char * titulo;
10 char * interprete;
11 int anyo;
12 TipoListaCanciones canciones;
13 struct Disco * sig;
14 };
15
16 typedef struct Disco * TipoColeccion;
Hemos optado por listas simplemente enlazadas y con puntero a cabeza.
Aqu´ı tienes una representaci´on gr´afica de una colecci´on con 3 discos compactos:
coleccion
1972
titulo
interprete
anyo
canciones
sig
1982
titulo
interprete
anyo
canciones
sig
1977
titulo
interprete
anyo
canciones
sig
titulo sig
titulo sig
titulo sig
titulo sig
titulo sig
titulo sig
titulo sig
titulo sig
I
0
g
1
n
2
a
3
c
4
i
5
o
6
0
7
L
0
o
1
g
2
o
3
s
4
0
5
D
0
o
1
m
2
i
3
n
4
i
5
o
6
n
7
0
8
O
0
g
1
u
2
n
3
d
4
e
5
0
6
T
0
o
1 2
b
3
e
4
0
5
O
0
f
1
f
2
e
3
r
4
i
5
n
6
g
7
0
8
E
0
x
1
p
2
r
3
e
4
s
5
s
6
i
7
o
8
n
9
0
10
N
0
u
1
m
2
b
3
e
4
r
5 6
o
7
n
8
e
9
0
10
E
0
x
1
p
2
r
3
e
4
s
5
s
6
i
7
o
8
n
9
0
10
J
0
o
1
h
2
n
3 4
C
5
o
6
l
7
t
8
r
9
a
10
n
11
e
12
0
13
L
0
o
1
g
2
o
3
s
4
0
5
T
0
a
1
n
2
g
3
e
4
r
5
i
6
n
7
e
8 9
D
10
r
11
e
12
a
13
m
14
0
15
I
0
g
1
n
2
a
3
c
4
i
5
o
6
0
7
V
0
a
1
n
2
g
3
e
4
l
5
i
6
s
7
0
8
Empezaremos por dise˜nar la estructura que corresponde a una lista de canciones. Despu´es
nos ocuparemos del dise˜no de registros del tipo ((disco compacto)). Y acabaremos definiendo un
tipo ((colecci´on de discos compactos)).
312 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
Vamos a dise˜nar funciones para gestionar listas de canciones. Lo que no vamos a hacer es
montar toda posible operaci´on sobre una lista. S´olo invertiremos esfuerzo en las operaciones que
se van a utilizar. ´Estas son:
Crear una lista vac´ıa.
A˜nadir una canci´on a la lista. (Durante la creaci´on de un disco iremos pidiendo las can-
ciones y a˜nadi´endolas a la ficha del disco.)
Mostrar la lista de canciones por pantalla. (Esta funci´on se usar´a cuando se muestre una
ficha detallada de un disco.)
Buscar una canci´on y decir si est´a o no est´a en la lista.
Borrar todas las canciones de la lista. (Cuando se elimine un disco de la base de datos
tendremos que liberar la memoria ocupada por todas sus canciones.)
La funci´on de creaci´on de una lista de canciones es trivial:
1 TipoListaCanciones crea_lista_canciones(void)
2 {
3 return NULL;
4 }
Pasemos a la funci´on que a˜nade una canci´on a una lista de canciones. No nos indican que las
canciones deban almacenarse en un orden determinado, as´ı que recurriremos al m´etodo m´as
sencillo: la inserci´on por cabeza.
1 TipoListaCanciones anyade_cancion(TipoListaCanciones lista, char titulo[])
2 {
3 struct Cancion * nuevo = malloc(sizeof(struct Cancion));
4
5 nuevo->titulo = malloc((strlen(titulo)+1)*sizeof(char));
6 strcpy(nuevo->titulo, titulo);
7 nuevo->sig = lista;
8 lista = nuevo;
9 return lista;
10 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 304 La verdad es que insertar las canciones por la cabeza es el m´etodo menos indicado,
pues cuando se recorra la lista para mostrarlas por pantalla aparecer´an en orden inverso a aqu´el
con el que fueron introducidas. Modifica anyade_cancion para que las canciones se inserten por
la cola.
· 305 Y ya que sugerimos que insertes canciones por cola, modifica las estructuras necesarias
para que la lista de canciones se gestione con una lista de registros con puntero a cabeza y cola.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Mostrar la lista de canciones es muy sencillo:
1 void muestra_canciones(TipoListaCanciones lista)
2 {
3 struct Cancion * aux;
4
5 for (aux=lista; aux!=NULL; aux=aux->sig)
6 printf (" %sn", aux->titulo);
7 }
Buscar una canci´on es un simple recorrido que puede terminar anticipadamente tan pronto
se encuentra el objeto buscado:
1 int contiene_cancion_con_titulo(TipoListaCanciones lista, char titulo[])
2 {
3 struct Cancion * aux;
4
5 for (aux=lista; aux!=NULL; aux=aux->sig)
Introducci´on a la Programaci´on con C 313
4.11 Una aplicaci´on: una base de datos para discos compactos
6 if (strcmp(aux->titulo, titulo)==0)
7 return 1;
8 return 0;
9 }
Borrar todas las canciones de una lista debe liberar la memoria propia de cada nodo, pero
tambi´en debe liberar la cadena que almacena cada t´ıtulo, pues tambi´en se solicit´o con malloc:
1 TipoListaCanciones libera_canciones(TipoListaCanciones lista)
2 {
3 struct Cancion * aux, * siguiente;
4
5 aux = lista;
6 while (aux != NULL) {
7 siguiente = aux->sig;
8 free(aux->titulo);
9 free(aux);
10 aux = siguiente;
11 }
12 return NULL;
13 }
No ha sido tan dif´ıcil. Una vez sabemos manejar listas, las aplicaciones pr´acticas se dise˜nan
reutilizando buena parte de las rutinas que hemos presentado en apartados anteriores.
Pasamos a encargarnos de las funciones que gestionan la lista de discos. Como es habitual,
empezamos con una funci´on que crea una colecci´on (una lista) vac´ıa:
1 TipoColeccion crea_coleccion(void)
2 {
3 return NULL;
4 }
A˜nadir un disco obliga a solicitar memoria tanto para el registro en s´ı como para algunos
de sus componentes: el t´ıtulo y el int´erprete:
1 TipoColeccion anyade_disco(TipoColeccion lista, char titulo[], char interprete[],
2 int anyo, TipoListaCanciones canciones)
3 {
4 struct Disco * disco;
5
6 disco = malloc(sizeof(struct Disco));
7 disco->titulo = malloc((strlen(titulo)+1)*sizeof(char));
8 strcpy(disco->titulo, titulo);
9 disco->interprete = malloc((strlen(interprete)+1)*sizeof(char));
10 strcpy(disco->interprete, interprete);
11 disco->anyo = anyo;
12 disco->canciones = canciones;
13 disco->sig = lista;
14 lista = disco;
15 return lista;
16 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 306 Modifica anyade_disco para que los discos est´en siempre ordenados alfab´eticamente
por int´erprete y, para cada int´erprete, por valor creciente del a˜no de edici´on.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Y la memoria solicitada debe liberarse ´ıntegramente: si al reservar memoria para un disco
ejecutamos tres llamadas a malloc, habr´a que efectuar tres llamadas a free:
1 TipoColeccion libera_coleccion(TipoColeccion lista)
2 {
3 struct Disco * aux, * siguiente;
4
5 aux = lista;
6 while (aux != NULL) {
314 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
7 siguiente = aux->sig;
8 free(aux->titulo);
9 free(aux->interprete);
10 aux->canciones = libera_canciones(aux->canciones);
11 free(aux);
12 aux = siguiente;
13 }
14 return NULL;
15 }
Mostrar por pantalla el contenido de un disco es sencillo, especialmente si usamos mues-
tra_canciones para mostrar la lista de canciones.
1 void muestra_disco(struct Disco eldisco)
2 {
3 printf ("T´ıtulo: %sn", eldisco.titulo);
4 printf ("Int´erprete: %sn", eldisco.interprete);
5 printf ("A~no de edici´on: %dn", eldisco.anyo);
6 printf ("Canciones:n");
7 muestra_canciones(eldisco.canciones);
8 }
Mostrar la colecci´on completa es trivial si usamos la funci´on que muestra un disco:
1 void muestra_coleccion(TipoColeccion lista)
2 {
3 struct Disco * aux;
4
5 for (aux=lista; aux!=NULL; aux=aux->sig)
6 muestra_disco(*aux);
7 }
Las funciones de b´usqueda de discos se usan en un contexto determinado: el de mostrar,
si se encuentra el disco, su contenido por pantalla. En lugar de hacer que la funci´on devuelva
el valor 1 o 0, podemos hacer que devuelva un puntero al registro cuando lo encuentre o NULL
cuando el disco no est´e en la base de datos. Aqu´ı tienes las funciones de b´usqueda por t´ıtulo y
por int´erprete:
1 struct Disco * busca_disco_por_titulo_disco(TipoColeccion coleccion, char titulo[])
2 {
3 struct Disco * aux;
4
5 for (aux=coleccion; aux!=NULL; aux=aux->sig)
6 if (strcmp(aux->titulo, titulo) == 0)
7 return aux;
8 return NULL;
9 }
10
11 struct Disco * busca_disco_por_interprete(TipoColeccion coleccion, char interprete[])
12 {
13 struct Disco * aux;
14
15 for (aux=coleccion; aux!=NULL; aux=aux->sig)
16 if (strcmp(aux->interprete, interprete) == 0)
17 return aux;
18 return NULL;
19 }
La funci´on de b´usqueda por t´ıtulo de canci´on es similar, s´olo que llama a la funci´on que busca
una canci´on en una lista de canciones:
1 struct Disco * busca_disco_por_titulo_cancion(TipoColeccion coleccion, char titulo[])
2 {
3 struct Disco * aux;
4
5 for (aux=coleccion; aux!=NULL; aux=aux->sig)
Introducci´on a la Programaci´on con C 315
4.11 Una aplicaci´on: una base de datos para discos compactos
6 if (contiene_cancion_con_titulo(aux->canciones, titulo))
7 return aux;
8 return NULL;
9 }
S´olo nos queda por definir la funci´on que elimina un disco de la colecci´on dado su t´ıtulo:
1 TipoColeccion borra_disco_por_titulo_e_interprete(TipoColeccion coleccion, char titulo[],
2 char interprete[])
3 {
4 struct Disco *aux, *atras;
5
6 for (atras = NULL, aux=coleccion; aux != NULL; atras = aux, aux = aux->sig)
7 if (strcmp(aux->titulo, titulo) == 0 && strcmp(aux->interprete, interprete) == 0) {
8 if (atras == NULL)
9 coleccion = aux->sig;
10 else
11 atras->sig = aux->sig;
12 free(aux->titulo);
13 free(aux->interprete);
14 aux->canciones = libera_canciones(aux->canciones);
15 free(aux);
16 return coleccion;
17 }
18 return coleccion;
19 }
Ya tenemos todas las herramientas para enfrentarnos al programa principal:
discoteca2.c discoteca2.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <ctype.h>
5
6 #define MAXCAD 1000
7
8 enum { Anyadir=1, BuscarPorTituloDisco, BuscarPorInterprete, BuscarPorTituloCancion,
9 Mostrar, EliminarDisco, Salir};
10
.
.
.
182
183 int main(void)
184 {
185 int opcion;
186 TipoColeccion coleccion;
187 char titulo_disco[MAXCAD+1], titulo_cancion[MAXCAD+1], interprete[MAXCAD+1];
188 char linea[MAXCAD+1];
189 int anyo;
190 struct Disco * undisco;
191 TipoListaCanciones lista_canciones;
192
193 coleccion = crea_coleccion();
194
195 do {
196 printf ("Men´un");
197 printf ("1) A~nadir discon");
198 printf ("2) Buscar por t´ıtulo del discon");
199 printf ("3) Buscar por int´erpreten");
200 printf ("4) Buscar por t´ıtulo de canci´onn");
201 printf ("5) Mostrar todon");
202 printf ("6) Eliminar un disco por t´ıtulo e int´erprten");
203 printf ("7) Finalizarn");
204 printf ("Opci´on: "); gets(linea); sscanf (linea, "%d", &opcion);
316 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
205
206 switch(opcion) {
207 case Anyadir:
208 printf ("T´ıtulo: "); gets(titulo_disco);
209 printf ("Int´erprete: "); gets(interprete);
210 printf ("A~no: "); gets(linea); sscanf (linea, "%d", &anyo);
211 lista_canciones = crea_lista_canciones();
212 do {
213 printf ("T´ıtulo de canci´on (pulse retorno para acabar): ");
214 gets(titulo_cancion);
215 if (strlen(titulo_cancion) > 0)
216 lista_canciones = anyade_cancion(lista_canciones, titulo_cancion);
217 } while (strlen(titulo_cancion) > 0);
218 coleccion = anyade_disco(coleccion, titulo_disco, interprete, anyo, lista_canciones);
219 break;
220
221 case BuscarPorTituloDisco:
222 printf ("T´ıtulo: "); gets(titulo_disco);
223 undisco = busca_disco_por_titulo_disco(coleccion, titulo_disco);
224 if (undisco != NULL)
225 muestra_disco(*undisco);
226 else
227 printf ("No hay discos con t´ıtulo ’%s’n", titulo_disco);
228 break;
229
230 case BuscarPorInterprete:
231 printf ("Int´erprete: "); gets(interprete);
232 undisco = busca_disco_por_interprete(coleccion, interprete);
233 if (undisco != NULL)
234 muestra_disco(*undisco);
235 else
236 printf ("No hay discos de %sn", interprete);
237 break;
238
239 case BuscarPorTituloCancion:
240 printf ("T´ıtulo: "); gets(titulo_cancion);
241 undisco = busca_disco_por_titulo_cancion(coleccion, titulo_cancion);
242 if (undisco != NULL)
243 muestra_disco(*undisco);
244 else
245 printf ("No hay discos con alguna canci´on titulada ’%s’n", titulo_cancion);
246 break;
247
248 case Mostrar:
249 muestra_coleccion(coleccion);
250 break;
251
252 case EliminarDisco:
253 printf ("T´ıtulo: "); gets(titulo_disco);
254 printf ("Int´erprete: "); gets(interprete);
255 coleccion = borra_disco_por_titulo_e_interprete(coleccion, titulo_disco, interprete);
256 break;
257 }
258 } while (opcion != Salir);
259
260 coleccion = libera_coleccion(coleccion);
261
262 return 0;
263 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 307 Modifica el programa para que se almacene la duraci´on de cada canci´on (en segundos)
junto al t´ıtulo de la misma.
Introducci´on a la Programaci´on con C 317
4.12 Otras estructuras de datos con registros enlazados
· 308 La funci´on de b´usqueda de discos por int´erprete se detiene al encontrar el primer
disco de un int´erprete dado. Modifica la funci´on para que devuelva una lista con una copia
de todos los discos de un int´erprete. Usa esa lista para mostrar su contenido por pantalla con
muestra_coleccion y elim´ınala una vez hayas mostrado su contenido.
· 309 Dise˜na una aplicaci´on para la gesti´on de libros de una biblioteca. Debes mantener
dos listas: una lista de libros y otra de socios. De cada socio recordamos el nombre, el DNI
y el tel´efono. De cada libro mantenemos los siguientes datos: t´ıtulo, autor, ISBN, c´odigo de
la biblioteca (una cadena con 10 caracteres) y estado. El estado es un puntero que, cuando
vale NULL, indica que el libro est´a disponible y, en caso contrario, apunta al socio al que se ha
prestado el libro.
El programa debe permitir dar de alta y baja libros y socios, as´ı como efectuar el pr´estamo
de un libro a un socio y gestionar su devoluci´on. Ten en cuenta que no es posible dar de baja
a un socio que posee un libro en pr´estamo ni dar de baja un libro prestado.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.12. Otras estructuras de datos con registros enlazados
La posibilidad de trabajar con registros enlazados abre las puertas al dise˜no de estructuras de
datos muy elaboradas que permiten efectuar ciertas operaciones muy eficientemente. El precio
a pagar es una mayor complejidad de nuestros programas C y, posiblemente, un mayor consumo
de memoria (estamos almacenando valores y punteros, aunque s´olo nos interesan los valores).
Pero no has visto m´as que el principio. En otras asignaturas de la carrera aprender´as a
utilizar estructuras de datos complejas, pero capaces de ofrecer tiempos de respuesta mucho
mejores que las listas que hemos estudiado o capaces de permitir implementaciones sencillas
para operaciones que a´un no hemos estudiado. Te vamos a presentar unos pocos ejemplos
ilustrativos.
Las listas circulares, por ejemplo, son listas sin final. El nodo siguiente al que parece el
´ultimo nodo es el primero. Ning´un nodo est´a ligado a NULL.
lista 3
info sig
8
info sig
2
info sig
Este tipo de estructura de datos es ´util, por ejemplo, para mantener una lista de tareas
a las que hay que ir dedicando atenci´on rotativamente: cuando hemos hecho una ronda,
queremos pasar nuevamente al primer elemento. El campo sig del ´ultimo elemento permite
pasar directamente al primero, con lo que resulta sencillo codificar un bucle que recorre
rotativamente la lista.
En muchas aplicaciones es preciso trabajar con matrices dispersas. Una matriz dispersa es
una matriz en la que muy pocos componentes presentan un valor diferente de cero. Esta
matriz, por ejemplo, es dispersa:
















0 0 2.5 0 0 1.2 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 3.7 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 1.3 8.1 0 0 0 0 0.2 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
















De los 100 componentes de esta matriz de 10 × 10, tan s´olo hay 6 no nulos. Las matri-
ces dispersas pueden representarse con listas de listas para ahorrar memoria. Una lista
mantiene las filas que, a su vez, son listas de valores no nulos. En estas ´ultimas listas,
cada nodo almacena la columna del valor no nulo y el propio valor. La matriz dispersa
del ejemplo se representar´ıa as´ı (suponiendo que filas y columnas empiezan numer´andose
en 1, como es habitual en matem´aticas):
318 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica
matriz
1
sig fila cols
3
2.5
columna
valor
sig
6
1.2
columna
valor
sig
3
sig fila cols
2
3.7
columna
valor
sig
6
sig fila cols
2
1.3
columna
valor
sig
3
8.1
columna
valor
sig
8
0.2
columna
valor
sig
El ahorro de memoria es notabil´ısimo: si un float ocupa 8 bytes, hemos pasado de 800 a
132 bytes consumidos. El ahorro es relativamente mayor cuanto mayor es la matriz. Eso
s´ı, la complejidad de los algoritmos que manipulan esa estructura es tambi´en notabil´ısima.
¡Imagina el procedimiento que permite multiplicar eficientemente dos matrices dispersas
representadas as´ı!
Un ´arbol binario de b´usqueda es una estructura montada con registros enlazados, pero no
es una lista. Cada nodo tiene cero, uno o dos hijos: uno a su izquierda y uno a su derecha.
Los nodos que no tienen hijos se llaman hojas. El nodo m´as alto, del que descienden todos
los dem´as, se llama nodo ra´ız. Los descendientes de un nodo (sus hijos, nietos, biznietos,
etc.) tienen una curiosa propiedad: si descienden por su izquierda, tienen valores m´as
peque˜nos que el de cualquier ancestro, y si descienden por su derecha, valores mayores.
Aqu´ı tienes un ejemplo de ´arbol binario de b´usqueda:
raiz
10
der info izq
3
der info izq
15
der info izq
1
der info izq
6
der info izq
12
der info izq
23
der info izq
Una ventaja de los ´arboles binarios de b´usqueda es la rapidez con que pueden resolver
la pregunta ((¿pertenece un valor determinado al conjunto de valores del ´arbol?)). Hay un
m´etodo recursivo que recibe un puntero a un nodo y dice:
• si el puntero vale NULL; la respuesta es no;
• si el valor coincide con el del nodo apuntado, la respuesta es s´ı;
• si el valor es menor que el valor del nodo apuntado, entonces la respuesta la conoce
el hijo izquierdo, por lo que se le pregunta a ´el (recursivamente);
• y si el valor es mayor que el valor del nodo apuntado, entonces la respuesta la conoce
el hijo derecho, por lo que se le pregunta a ´el (recursivamente).
Ingenioso, ¿no? Observa que muy pocos nodos participan en el c´alculo de la respuesta. Si
deseas saber, por ejemplo, si el 6 pertenece al ´arbol de la figura, s´olo hay que preguntarle
a los nodos que tienen el 10, el 3 y el 6. El resto de nodos no se consultan para nada.
Siempre es posible responder a una pregunta de pertenencia en un ´arbol con n nodos
visitando un n´umero de nodos que es, a lo sumo, igual a 1 + log2 n. Rapid´ısimo. ¿Qu´e
costar´a, a cambio, insertar o borrar un nodo en el ´arbol? Cabe pensar que mucho m´as
que un tiempo proporcional al n´umero de nodos, pues la estructura de los enlaces es muy
compleja. Pero no es as´ı. Existen procedimientos sofisticados que consiguen efectuar esas
operaciones en tiempo proporcional ¡al logaritmo en base 2 del n´umero de nodos!
Introducci´on a la Programaci´on con C 319
4.12 Otras estructuras de datos con registros enlazados
Hay muchas m´as estructuras de datos que permiten acelerar sobremanera los programas
que gestionan grandes conjuntos de datos. Apenas hemos empezado a conocer y aprendido a
manejar las herramientas con las que se construyen los programas: las estructuras de datos y
los algoritmos.
320 Introducci´on a la Programaci´on con C
Cap´ıtulo 5
Ficheros
—Me temo que s´ı, se˜nora —dijo Alicia—. No recuerdo las cosas como sol´ıa. . . ¡y no
conservo el mismo tama˜no diez minutos seguidos!
Lewis Carroll, Alicia en el Pa´ıs de las Maravillas.
Acabamos nuestra introducci´on al lenguaje C con el mismo objeto de estudio con el que finaliza-
mos la presentaci´on del lenguaje Python: los ficheros. Los ficheros permiten guardar informaci´on
en un dispositivo de almacenamiento de modo que ´esta ((sobreviva)) a la ejecuci´on de un pro-
grama. No te vendr´ıa mal repasar los conceptos introductorios a ficheros antes de empezar.
5.1. Ficheros de texto y ficheros binarios
Con Python estudiamos ´unicamente ficheros de texto. Con C estudiaremos dos tipos de ficheros:
ficheros de texto y ficheros binarios.
5.1.1. Representaci´on de la informaci´on en los ficheros de texto
Ya conoces los ficheros de texto: contienen datos legibles por una persona y puedes generarlos
o modificarlos desde tus propios programas o usando aplicaciones como los editores de texto.
Los ficheros binarios, por contra, no est´an pensados para facilitar su lectura por parte de seres
humanos (al menos no directamente).
Pongamos que se desea guardar un valor de tipo entero en un fichero de texto, por ejemplo,
el valor 12. En el fichero de texto se almacenar´a el d´ıgito ’1’ (codificado en ASCII como el valor
49) y el d´ıgito ’2’ (codificado en ASCII como el valor 50), es decir, dos datos de tipo char. A la
hora de leer el dato, podremos leerlo en cualquier variable de tipo entero con capacidad suficiente
para almacenar ese valor (un char, un unsigned char, un int, un unsigned int, etc.). Esto es
as´ı porque la lectura de ese dato pasa por un proceso de interpretaci´on relativamente sofisticado:
cuando se lee el car´acter ’1’, se memoriza el valor 1; y cuando se lee el car´acter ’2’, se multiplica
por 10 el valor memorizado y se le suma el valor 2. As´ı se llega al valor 12, que es lo que se
almacena en la variable en cuesti´on. Observa que, codificado como texto, 12 ocupa dos bytes,
pero que si se almacena en una variable de tipo char ocupa 1 y en una variable de tipo int
ocupa 4.
Un problema de los ficheros de texto es la necesidad de usar marcas de separaci´on entre
sus diferentes elementos. Si, por ejemplo, al valor 12 ha de sucederle el valor 100, no podemos
limitarnos a disponer uno a continuaci´on del otro sin m´as, pues el fichero contendr´ıa la siguiente
secuencia de caracteres:
1 2 1 0 0
¿Qu´e estamos representando exactamente? ¿Un 12 seguido de un 100 o un 1 seguido de un
2100? ¿Y por qu´e no un 1210 seguido de un 0 o, sencillamente, el valor 12100, sin m´as?
Las marcas de separaci´on son caracteres que decide el programador, pero es corriente que
se trate de espacios en blanco, tabuladores o saltos de l´ınea. El valor 12 seguido del valor 100
podr´ıa representarse, pues, con cualquiera de estas secuencias de caracteres:
Introducci´on a la Programaci´on con C 321
5.1 Ficheros de texto y ficheros binarios
1 2 1 0 0
1 2 t 1 0 0
1 2 n 1 0 0
Usar caracteres separadores es fuente, naturalmente, de un coste adicional: un mayor tama˜no
de los ficheros.
Cuando los separadores son espacios en blanco, es frecuente permitir libertad en cuanto a
su n´umero:
1 2 n 1 0 0 n
Las herramientas con las que leemos los datos de ficheros de texto saben lidiar con las compli-
caciones que introducen estos separadores blancos repetidos.
Los ficheros de texto cuentan con la ventaja de que se pueden inspeccionar con ayuda de
un editor de texto y permiten as´ı, por lo general, deducir el tipo de los diferentes datos que lo
componen, pues ´estos resultan legibles.
5.1.2. Representaci´on de la informaci´on en los ficheros binarios
Los ficheros binarios requieren una mayor precisi´on en la determinaci´on de la codificaci´on de
la informaci´on. Si almacenamos el valor 12 en un fichero binario, hemos de decidir si queremos
almacenarlo como car´acter con o sin signo, como entero con o sin signo, etc. La decisi´on adoptada
determinar´a la ocupaci´on de la informaci´on (uno o cuatro bytes) y su codificaci´on (binario
natural o complemento a dos). Si guardamos el 12 como un char, guardaremos un solo byte
formado por estos 8 bits:
00001100
Pero si optamos por almacenarlo como un int, ser´an cuatro los bytes escritos:
00000000 00000000 00000000 00001100
Un mismo patr´on de 8 bits, como
11111111
tiene dos interpretaciones posibles: el valor 255 si entendemos que es un dato de tipo unsig-
ned char o el valor −1 si consideramos que codifica un dato de tipo char.1
Como puedes ver, la secuencia de bits que escribimos en el fichero es exactamente la misma
que hay almacenada en la memoria, usando la mism´ısima codificaci´on binaria. De ah´ı el nombre
de ficheros binarios.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 310 ¿Qu´e ocupa en un fichero de texto cada uno de estos datos?
a) 1
b) 0
c) 12
d) -15
e) 128
f) 32767
g) -32768
h) 2147483647
i) -2147483648
¿Y cu´anto ocupa cada uno de ellos si los almacenamos en un fichero binario como valores
de tipo int?
1Un fichero de texto no presentar´ıa esta ambig¨uedad: el n´umero se habr´ıa escrito como −1 o como 255. S´ı
que presentar´ıa, sin embargo, un punto de elecci´on reservado al programador: aunque −1 lleva signo y por tanto
se almacenar´a en una variable de alg´un tipo con signo, ¿queremos almacenarlo en una variable de tipo char,
una variable de tipo int o, por qu´e no, en una variable de tipo float?
322 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros
· 311 ¿C´omo se interpreta esta secuencia de bytes en cada uno de los siguientes supuestos?
00000000 00000000 00000000 00001100
a) Como cuatro datos de tipo char.
b) Como cuatro datos de tipo unsigned char.
c) Como un dato de tipo int.
d) Como un dato de tipo unsigned int.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Escribir dos o m´as datos de un mismo tipo en un fichero binario no requiere la inserci´on de
marcas separadoras: cada cierto n´umero de bytes empieza un nuevo dato (cada cuatro bytes,
por ejemplo, empieza un nuevo int), as´ı que es f´acil decidir d´onde empieza y acaba cada dato.
La lectura de un fichero binario requiere un conocimiento exacto del tipo de datos de cada
uno de los valores almacenados en ´el, pues de lo contrario la secuencia de bits carecer´a de un
significado definido.
Los ficheros binarios no s´olo pueden almacenar escalares. Puedes almacenar tambi´en regis-
tros y vectores pues, a fin de cuentas, no son m´as que patrones de bits de tama˜no conocido.
Lo ´unico que no debe almacenarse en ficheros binarios son los punteros. La raz´on es sencilla:
si un puntero apunta a una zona de memoria reservada con malloc, su valor es la direcci´on del
primer byte de esa zona. Si guardamos ese valor en disco y lo recuperamos m´as tarde (en una
ejecuci´on posterior, por ejemplo), esa zona puede que no haya sido reservada. Acceder a ella
provocar´a, en consecuencia, un error capaz de abortar la ejecuci´on del programa.
Por regla general, los ficheros binarios son m´as compactos que los ficheros de texto, pues
cada valor ocupa lo mismo que ocupar´ıa en memoria. La lectura (y escritura) de los datos de
ficheros binarios es tambi´en m´as r´apida, ya que nos ahorramos el proceso de conversi´on del
formato de texto al de representaci´on de informaci´on en memoria y viceversa. Pero no todo son
ventajas.
Portabilidad de ficheros
Los ficheros binarios presentan algunos problemas de portabilidad, pues no todos los or-
denadores almacenan en memoria los valores num´ericos de la misma forma: los ficheros
binarios escritos en un ordenador ((big-endian)) no son directamente legibles en un ordenador
((little-endian)).
Los ficheros de texto son, en principio, m´as portables, pues la tabla ASCII es un est´andar
ampliamente aceptado para el intercambio de ficheros de texto. No obstante, la tabla ASCII
es un c´odigo de 7 bits que s´olo da cobertura a los s´ımbolos propios de la escritura del ingl´es
y algunos caracteres especiales. Los caracteres acentuados, por ejemplo, est´an excluidos. En
los ´ultimos a˜nos se ha intentado implantar una familia de est´andares que den cobertura a
estos y otros caracteres. Como 8 bits resultan insuficientes para codificar todos los caracteres
usados en la escritura de cualquier lenguaje, hay diferentes subconjuntos para cada una de
las diferentes comunidades culturales. Las lenguas rom´anicas occidentales usan el est´andar
IsoLatin-1 (o ISO-8859-1), recientemente ampliado con el s´ımbolo del euro para dar lugar
al IsoLatin-15 (o ISO-8859-15). Los problemas de portabilidad surgen cuando interpretamos
un fichero de texto codificado con IsoLatin-1 como si estuviera codificado con otro est´andar:
no veremos m´as que un galimat´ıas de s´ımbolos extra˜nos all´ı donde se usan caracteres no
ASCII.
5.2. Ficheros de texto
5.2.1. Abrir, leer/escribir, cerrar
Los ficheros de texto se manipulan en C siguiendo el mismo ((protocolo)) que segu´ıamos en
Python:
1. Se abre el fichero en modo lectura, escritura, adici´on, o cualquier otro modo v´alido.
Introducci´on a la Programaci´on con C 323
5.2 Ficheros de texto
2. Se trabaja con ´el leyendo o escribiendo datos, seg´un el modo de apertura escogido. Al
abrir un fichero se dispone un ((cabezal)) de lectura o escritura en un punto definido del
fichero (el principio o el final). Cada acci´on de lectura o escritura desplaza el cabezal de
izquierda a derecha, es decir, de principio a final del fichero.
3. Se cierra el fichero.
Bueno, lo cierto es que, como siempre en C, hay un paso adicional y previo a estos tres: la
declaraci´on de una variable de ((tipo fichero)). La cabecera stdio.h incluye la definici´on de
un tipo de datos llamado FILE y declara los prototipos de las funciones de manipulaci´on de
ficheros. Nuestra variable de tipo fichero ha de ser un puntero a FILE, es decir, ha de ser de
tipo FILE *.
Las funciones b´asicas con las que vamos a trabajar son:
fopen: abre un fichero. Recibe la ruta de un fichero (una cadena) y el modo de apertura
(otra cadena) y devuelve un objeto de tipo FILE *.
FILE * fopen (char ruta[], char modo[]);
Los modos de apertura para ficheros de texto con los que trabajaremos son ´estos:
• "r" (lectura): El primer car´acter le´ıdo es el primero del fichero.
• "w" (escritura): Trunca el fichero a longitud 0. Si el fichero no existe, se crea.
• "a" (adici´on): Es un modo de escritura que preserva el contenido original del fichero.
Los caracteres escritos se a˜naden al final del fichero.
Si el fichero no puede abrirse por cualquier raz´on, fopen devuelve el valor NULL. (Observa
que los modos se indican con cadenas, no con caracteres: debes usar comillas dobles.)
Modos de apertura para lectura y escritura simult´anea
Los modos "r", "w" y "a" no son los ´unicos v´alidos para los ficheros de texto. Puedes usar,
adem´as, ´estos otros: "r+", "w+" y "a+". Todos ellos abren los ficheros en modo de lectura
y escritura a la vez. Hay, no obstante, matices que los diferencian:
• "r+": No se borra el contenido del fichero, que debe existir previamente. El ((cabezal))
de lectura/escritura se sit´ua al principio del fichero.
• "w+": Si el fichero no existe, se crea, y si existe, se trunca el contenido a longitud
cero. El ((cabezal)) de lectura/escritura se sit´ua al principio del fichero.
• "a+": Si el fichero no existe, se crea. El ((cabezal)) de lectura/escritura se sit´ua al final
del fichero.
Una cosa es que existan estos m´etodos y otra que te recomendemos su uso. Te lo
desaconsejamos. Resulta muy dif´ıcil escribir en medio de un fichero de texto a voluntad sin
destruir la informaci´on previamente existente en ´el, pues cada l´ınea puede ocupar un n´umero
de caracteres diferente.
fclose: cierra un fichero. Recibe el FILE * devuelto por una llamada previa a fopen.
int fclose (FILE * fichero);
El valor devuelto por fclose es un c´odigo de error que nos advierte de si hubo un fallo al
cerrar el fichero. El valor 0 indica ´exito y el valor EOF (predefinido en stdio.h) indica error.
M´as adelante indicaremos c´omo obtener informaci´on adicional acerca del error detectado.
Cada apertura de un fichero con fopen debe ir acompa˜nada de una llamada a fclose una
vez se ha terminado de trabajar con el fichero.
fscanf : lee de un fichero. Recibe un fichero abierto con fopen (un FILE *), una cadena
de formato (usando las marcas de formato que ya conoces por scanf ) y las direcciones
de memoria en las que debe depositar los valores le´ıdos. La funci´on devuelve el n´umero
de elementos efectivamente le´ıdos (valor que puedes usar para comprobar si la lectura se
complet´o con ´exito).
324 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros
int fscanf (FILE * fichero, char formato[], direcciones );
fprintf : escribe en un fichero. Recibe un fichero abierto con fopen (un FILE *), una cade-
na de formato (donde puedes usar las marcas de formato que aprendiste a usar con printf )
y los valores que deseamos escribir. La funci´on devuelve el n´umero de caracteres efectiva-
mente escritos (valor que puedes usar para comprobar si se escribieron correctamente los
datos).
int fprintf (FILE * fichero, char formato[], valores );
feof : devuelve 1 si estamos al final del fichero y 0 en caso contrario. El nombre de la
funci´on es abreviatura de ((end of file)) (en espa˜nol, ((fin de fichero))). ¡Ojo! S´olo tiene
sentido consultar si se est´a o no al final de fichero tras efectuar una lectura de datos.
(Este detalle complicar´a un poco las cosas.)
int feof (FILE * fichero);
Como puedes ver no va a resultar muy dif´ıcil trabajar con ficheros de texto en C. A fin de
cuentas, las funciones de escritura y lectura son b´asicamente id´enticas a printf y scanf , y ya
hemos aprendido a usarlas. La ´unica novedad destacable es la nueva forma de detectar si hemos
llegado al final de un fichero o no: ya no se devuelve la cadena vac´ıa como consecuencia de una
lectura al final del fichero, como ocurr´ıa en Python, sino que hemos de preguntar expl´ıcitamente
por esa circunstancia usando una funci´on (feof ).
Nada mejor que un ejemplo para aprender a utilizar ficheros de texto en C. Vamos a generar
los 1000 primeros n´umeros primos y a guardarlos en un fichero de texto. Cada n´umero se
escribir´a en una l´ınea.
genera primos.c genera primos.c
1 #include <stdio.h>
2
3 int es_primo(int n)
4 {
5 int i, j, primo;
6 primo = 1;
7 for (j=2; j<=n/2; j++)
8 if (n % j == 0) {
9 primo = 0;
10 break;
11 }
12 return primo;
13 }
14
15 int main(void)
16 {
17 FILE * fp;
18 int i, n;
19
20 fp = fopen("primos.txt", "w");
21 i = 1;
22 n = 0;
23 while (n<1000) {
24 if (es_primo(i)) {
25 fprintf (fp, "%dn", i);
26 n++;
27 }
28 i++;
29 }
30 fclose(fp);
31
32 return 0;
33 }
Introducci´on a la Programaci´on con C 325
5.2 Ficheros de texto
Hemos llamado a la variable de fichero fp por ser abreviatura del t´ermino ((file pointer))
(puntero a fichero). Es frecuente utilizar ese nombre para las variables de tipo FILE *.
Una vez compilado y ejecutado el programa genera primos obtenemos un fichero de texto
llamado primos.txt del que te mostramos sus primeras y ´ultimas l´ıneas (puedes comprobar la
correcci´on del programa abriendo el fichero primos.txt con un editor de texto):
primos.txt
1 1
2 2
3 3
4 5
5 7
6 11
7 13
8 17
9 19
10 23
...
990 7823
991 7829
992 7841
993 7853
994 7867
995 7873
996 7877
997 7879
998 7883
999 7901
1000 7907
Aunque en pantalla lo vemos como una secuencia de l´ıneas, no es m´as que una secuencia de
caracteres:
1 n 2 n 3 n 5 n . . . 7 9 0 1 n 7 9 0 7 n
Dise˜nemos ahora un programa que lea el fichero primos.txt generado por el programa
anterior y muestre por pantalla su contenido:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE * fp;
6 int i;
7
8 fp = fopen("primos.txt", "r");
9 fscanf (fp, "%d", &i);
10 while ( !feof (fp) ) {
11 printf ("%dn", i);
12 fscanf (fp, "%d", &i);
13 }
14 fclose(fp);
15
16 return 0;
17 }
Observa que la llamada a fscanf se encuentra en un bucle que se lee as´ı ((mientras no se
haya acabado el fichero. . . )), pues feof averigua si hemos llegado al final del fichero. La l´ınea 9
contiene una lectura de datos para que la consulta a feof tenga sentido: feof s´olo actualiza
su valor tras efectuar una operaci´on de lectura del fichero. Si no te gusta la aparici´on de dos
sentencias fscanf , puedes optar por esta alternativa:
326 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE * fp;
6 int i;
7
8 fp = fopen("primos.txt", "r");
9 while (1) {
10 fscanf (fp, "%d", &i);
11 if (feof (fp)) break;
12 printf ("%dn", i);
13 }
14 fclose(fp);
15
16 return 0;
17 }
Y si deseas evitar el uso de break, considera esta otra:
lee primos.c lee primos.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE * fp;
6 int i;
7
8 fp = fopen("primos.txt", "r");
9 do {
10 fscanf (fp, "%d", &i);
11 if (!feof (fp))
12 printf ("%dn", i);
13 } while (!feof (fp));
14 fclose(fp);
15
16 return 0;
17 }
¿Y si el fichero no existe?
Al abrir un fichero puede que detectes un error: fopen devuelve la direcci´on NULL. Hay varias
razones, pero una que te ocurrir´a al probar algunos de los programas del texto es que el
fichero que se pretende leer no existe. Una soluci´on puede consistir en crearlo en ese mismo
instante:
1 f = fopen(ruta, "r");
2 if (f == NULL) {
3 f = fopen(ruta, "w");
4 fclose(f);
5 f = fopen(ruta, "r");
6 }
Si el problema era la inexistencia del fichero, este truco funcionar´a, pues el modo "w" lo
crea cuando no existe.
Es posible, no obstante, que incluso este m´etodo falle. En tal caso, es probable que tengas
un problema de permisos: ¿tienes permiso para leer ese fichero?, ¿tienes permiso para escribir
en el directorio en el que reside o debe residir el fichero? M´as adelante prestaremos atenci´on
a esta cuesti´on.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 312 Dise˜na un programa que a˜nada al fichero primos.txt los 100 siguientes n´umeros pri-
mos. El programa leer´a el contenido actual del fichero para averiguar cu´al es el ´ultimo primo del
Introducci´on a la Programaci´on con C 327
5.2 Ficheros de texto
fichero. A continuaci´on, abrir´a el fichero en modo adici´on ("a") y a˜nadir´a 100 nuevos primos. Si
ejecut´asemos una vez genera primos y, a continuaci´on, dos veces el nuevo programa, el fichero
acabar´ıa conteniendo los 1200 primeros primos.
· 313 Dise˜na un programa que lea de teclado una frase y escriba un fichero de texto llamado
palabras.txt en el que cada palabra de la frase ocupa una l´ınea.
· 314 Dise˜na un programa que lea de teclado una frase y escriba un fichero de texto llamado
letras.txt en el que cada l´ınea contenga un car´acter de la frase.
· 315 Modifica el programa miniGalaxis para que gestione una lista de records. Un fichero
de texto, llamado minigalaxis.records almacenar´a el nombre y n´umero de movimientos de
los 5 mejores jugadores de todos los tiempos (los que completaron el juego usando el menor
n´umero de sondas).
· 316 Disponemos de dos ficheros: uno contiene un diccionario y el otro, un texto. El diccio-
nario est´a ordenado alfab´eticamente y contiene una palabra en cada l´ınea. Dise˜na un programa
que lea el diccionario en un vector de cadenas y lo utilice para detectar errores en el texto. El
programa mostrar´a por pantalla las palabras del texto que no est´an en el diccionario, indicando
los n´umeros de l´ınea en que aparecen.
Supondremos que el diccionario contiene, a lo sumo, 1000 palabras y que la palabra m´as
larga (tanto en el diccionario como en el texto) ocupa 30 caracteres.
(Si quieres usar un diccionario real como el descrito y trabajas en Unix, encontrar´as uno en
ingl´es en /usr/share/dict/words o /usr/dict/words. Puedes averiguar el n´umero de palabras
que contiene con el comando wc de Unix.)
· 317 Modifica el programa del ejercicio anterior para que el n´umero de palabras del vector que
las almacena se ajuste autom´aticamente al tama˜no del diccionario. Tendr´as que usar memoria
din´amica.
Si usas un vector de palabras, puedes efectuar dos pasadas de lectura en el fichero que
contiene el diccionario: una para contar el n´umero de palabras y saber as´ı cu´anta memoria es
necesaria y otra para cargar la lista de palabras en un vector din´amico. Naturalmente, antes de
la segunda lectura deber´as haber reservado la memoria necesaria.
Una alternativa a leer dos veces el fichero consiste en usar realloc juiciosamente: reserva
inicialmente espacio para, digamos, 1000 palabras; si el diccionario contiene un n´umero de
palabras mayor que el que cabe en el espacio de memoria reservada, duplica la capacidad del
vector de palabras (cuantas veces sea preciso si el problema se da m´as de una vez).
Otra posibilidad es usar una lista simplemente enlazada, pues puedes crearla con una primera
lectura. Sin embargo, no es recomendable que sigas esta estrategia, pues no podr´as efectuar una
b´usqueda dicot´omica a la hora de determinar si una palabra est´a incluida o no en el diccionario.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Ya vimos en su momento que fscanf presenta un problema cuando leemos cadenas: s´olo
lee una ((palabra)), es decir, se detiene al llegar a un blanco. Aprendimos a usar entonces una
funci´on, gets, que le´ıa una l´ınea completa. Hay una funci´on equivalente para ficheros de texto:
char * fgets(char cadena[], int max_tam, FILE * fichero );
¡Ojo con el prototipo de fgets! ¡El par´ametro de tipo FILE * es el ´ultimo, no el primero! Otra
incoherencia de C. El primer par´ametro es la cadena en la que se desea depositar el resultado de
la lectura. El segundo par´ametro, un entero, es una medida de seguridad: es el m´aximo n´umero
de bytes que queremos leer en la cadena. Ese l´ımite permite evitar peligrosos desbordamientos
de la zona de memoria reservada para cadena cuando la cadena le´ıda es m´as larga de lo previsto.
El ´ultimo par´ametro es, finalmente, el fichero del que vamos a leer (previamente se ha abierto
con fopen). La funci´on se ocupa de terminar correctamente la cadena le´ıda con un ’0’, pero
respetando el salto de l´ınea (n) si lo hubiera.2
En caso de querer suprimir el retorno de l´ınea,
puedes invocar una funci´on como ´esta sobre la cadena le´ıda:
1 void quita_fin_de_linea(char linea[])
2 {
3 int i;
4 for (i=0; linea[i] != ’0’; i++)
5 if (linea[i] == ’n’) {
6 linea[i] = ’0’;
2En esto se diferencia de gets.
328 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros
7 break;
8 }
9 }
La funci´on fgets devuelve una cadena (un char *). En realidad, es un puntero a la propia
variable cadena cuando todo va bien, y NULL cuando no se ha podido efectuar la lectura. El valor
de retorno es ´util, ´unicamente, para hacer detectar posibles errores tras llamar a la funci´on.
Hay m´as funciones de la familia get. La funci´on fgetc, por ejemplo, lee un car´acter:
int fgetc(FILE * fichero);
No te equivoques: devuelve un valor de tipo int, pero es el valor ASCII de un car´acter. Puedes
asignar ese valor a un unsigned char, excepto cuando vale EOF (de ((end of file))), que es una
constante (cuyo valor es −1) que indica que no se pudo leer el car´acter requerido porque llegamos
al final del fichero.
Las funciones fgets y fgetc se complementan con fputs y fputc, que en lugar de leer una
cadena o un car´acter, escriben una cadena o un car´acter en un fichero abierto para escritura o
adici´on. He aqu´ı sus prototipos:
int fputs(char cadena[], FILE * fichero);
int fputc(int caracter, FILE * fichero);
Al escribir una cadena con fputs, el terminador ’0’ no se escribe en el fichero. Pero no te
preocupes: fgets ((lo sabe)) y lo introduce autom´aticamente en el vector de caracteres al leer del
fichero.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 318 Hemos escrito este programa para probar nuestra comprensi´on de fgets y fputs (presta
atenci´on tambi´en a los blancos, que se muestran con el car´acter ):
1 #include <stdio.h>
2 #include <string.h>
3
4 #define MAXLON 100
5
6 int main (void)
7 {
8 FILE * f;
9 char s[MAXLON+1];
10 char * aux;
11
12 f = fopen("prueba.txt", "w");
13 fputs("si", f);
14 fputs("non", f);
15 fclose(f);
16
17 f = fopen("prueba.txt", "r");
18 aux = fgets(s, MAXLON, f);
19 printf ("%s %sn", aux, s);
20 aux = fgets(s, MAXLON, f);
21 printf ("%s %sn", aux, s);
22 fclose(f);
23
24 return 0;
25 }
Primera cuesti´on: ¿Cu´antos bytes ocupa el fichero prueba.txt?
Al ejecutarlo, obtenemos este resultado en pantalla:
sino
sino
(null) sino
Segunda cuesti´on: ¿Puedes explicar con detalle qu´e ha ocurrido? (El texto (((null))) es
escrito autom´aticamente por printf cuando se le pasa como cadena un puntero a NULL.)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Introducci´on a la Programaci´on con C 329
5.2 Ficheros de texto
5.2.2. Aplicaciones: una agenda y un gestor de una colecci´on de discos
compactos
Lo aprendido nos permite ya dise˜nar programas capaces de escribir y leer colecciones de datos
en ficheros de texto.
Una agenda
Vamos a desarrollar un peque˜no ejemplo centrado en las rutinas de entrada/salida para la ges-
ti´on de una agenda montada con una lista simplemente enlazada. En la agenda, que cargaremos
de un fichero de texto, tenemos el nombre, la direcci´on y el tel´efono de varias personas. Cada
entrada en la agenda se representar´a con tres l´ıneas del fichero de texto. He aqu´ı un ejemplo de
fichero con este formato:
agenda.txt agenda.txt
1 Juan Gil
2 Ronda Mijares, 1220
3 964 123456
4 Ana Garc´ıa
5 Plaza del Sol, 13
6 964-872777
7 Pepe P´erez
8 Calle de Arriba, 1
9 964 263 263
Nuestro programa podr´a leer en memoria los datos de un fichero como ´este y tambi´en escribirlos
en fichero desde memoria.
Las estructuras de datos que manejaremos en memoria se definen as´ı:
1 struct Entrada {
2 char * nombre;
3 char * direccion;
4 char * telefono;
5 };
6
7 struct NodoAgenda {
8 struct Entrada datos;
9 struct NodoAgenda * sig;
10 };
11
12 typedef struct NodoAgenda * TipoAgenda;
Al final del apartado presentamos el programa completo. Centr´emonos ahora en las funciones
de escritura y lectura del fichero. La rutina de escritura de datos en un fichero recibir´a la
estructura y el nombre del fichero en el que guardamos la informaci´on. Guardaremos cada
entrada de la agenda en tres l´ıneas: una por cada campo.
1 void escribe_agenda(TipoAgenda agenda, char nombre_fichero[])
2 {
3 struct NodoAgenda * aux;
4 FILE * fp;
5
6 fp = fopen(nombre_fichero, "w");
7 for (aux=agenda; aux!=NULL; aux=aux->sig)
8 fprintf (fp, "%sn%sn%sn", aux->datos.nombre,
9 aux->datos.direccion,
10 aux->datos.telefono);
11 fclose(fp);
12 }
La lectura del fichero ser´a sencilla:
1 TipoAgenda lee_agenda(char nombre_fichero[])
2 {
330 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros
3 TipoAgenda agenda;
4 struct Entrada * entrada_leida;
5 FILE * fp;
6 char nombre[MAXCADENA+1], direccion[MAXCADENA+1], telefono[MAXCADENA+1];
7 int longitud;
8
9 agenda = crea_agenda();
10
11 fp = fopen(nombre_fichero, "r");
12 while (1) {
13 fgets(nombre, MAXCADENA, fp);
14 if (feof (fp)) break; // Si se acab´o el fichero, acabar la lectura.
15 quita_fin_de_linea(nombre);
16
17 fgets(direccion, MAXCADENA, fp);
18 quita_fin_de_linea(direccion);
19
20 fgets(telefono, MAXCADENA, fp);
21 quita_fin_de_linea(telefono);
22
23 agenda = anyadir_entrada(agenda, nombre, direccion, telefono);
24 }
25 fclose(fp);
26
27 return agenda;
28 }
La ´unica cuesti´on rese˜nable es la purga de saltos de l´ınea innecesarios.
He aqu´ı el listado completo del programa:
agenda sencilla.c agenda sencilla.c
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 #define MAXCADENA 200
5
6 enum { Ver=1, Alta, Buscar, Salir };
7
8 struct Entrada {
9 char * nombre;
10 char * direccion;
11 char * telefono;
12 };
13
14 struct NodoAgenda {
15 struct Entrada datos;
16 struct NodoAgenda * sig;
17 };
18
19 typedef struct NodoAgenda * TipoAgenda;
20
21 void quita_fin_de_linea(char linea[])
22 {
23 int i;
24 for (i=0; linea[i] != ’0’; i++)
25 if (linea[i] == ’n’) {
26 linea[i] = ’0’;
27 break;
28 }
29 }
30
31 void muestra_entrada(struct NodoAgenda * e)
32 // Podr´ıamos haber pasado e por valor, pero resulta m´as eficiente (y no mucho m´as
33 // inc´omodo) hacerlo por referencia: pasamos as´ı s´olo 4 bytes en lugar de 12.
Introducci´on a la Programaci´on con C 331
5.2 Ficheros de texto
34 {
35 printf ("Nombre : %sn", e->datos.nombre);
36 printf ("Direcci´on: %sn", e->datos.direccion);
37 printf ("Tel´efono : %sn", e->datos.telefono);
38 }
39
40 void libera_entrada(struct NodoAgenda * e)
41 {
42 int i;
43
44 free(e->datos.nombre);
45 free(e->datos.direccion);
46 free(e->datos.telefono);
47 free(e);
48 }
49
50
51 TipoAgenda crea_agenda(void)
52 {
53 return NULL;
54 }
55
56 TipoAgenda anyadir_entrada(TipoAgenda agenda, char nombre[],
57 char direccion[], char telefono[])
58 {
59 struct NodoAgenda * aux, * e;
60
61 /* Averiguar si ya tenemos una persona con ese nombre */
62 if (buscar_entrada_por_nombre(agenda, nombre) != NULL)
63 return agenda;
64
65 /* Si llegamos aqu´ı, es porque no ten´ıamos registrada a esa persona. */
66 e = malloc(sizeof(struct NodoAgenda));
67 e->datos.nombre = malloc((strlen(nombre)+1)*sizeof(char));
68 strcpy(e->datos.nombre, nombre);
69 e->datos.direccion = malloc((strlen(direccion)+1)*sizeof(char));
70 strcpy(e->datos.direccion, direccion);
71 e->datos.telefono = malloc((strlen(telefono)+1)*sizeof(char));
72 strcpy(e->datos.telefono, telefono);
73 e->sig = agenda;
74 agenda = e;
75 return agenda;
76 }
77
78 void muestra_agenda(TipoAgenda agenda)
79 {
80 struct NodoAgenda * aux;
81
82 for (aux = agenda; aux != NULL; aux = aux->sig)
83 muestra_entrada(aux);
84 }
85
86 struct NodoAgenda * buscar_entrada_por_nombre(TipoAgenda agenda, char nombre[])
87 {
88 struct NodoAgenda * aux;
89
90 for (aux = agenda; aux != NULL; aux = aux->sig)
91 if (strcmp(aux->datos.nombre, nombre) == 0)
92 return aux;
93
94 return NULL;
95 }
96
332 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros
97 void libera_agenda(TipoAgenda agenda)
98 {
99 struct NodoAgenda * aux, *siguiente;
100
101 aux = agenda;
102 while (aux != NULL) {
103 siguiente = aux->sig;
104 libera_entrada(aux);
105 aux = siguiente;
106 }
107 }
108
109 void escribe_agenda(TipoAgenda agenda, char nombre_fichero[])
110 {
111 struct NodoAgenda * aux;
112 FILE * fp;
113
114 fp = fopen(nombre_fichero, "w");
115 for (aux=agenda; aux!=NULL; aux=aux->sig)
116 fprintf (fp, "%sn%sn%sn", aux->datos.nombre,
117 aux->datos.direccion,
118 aux->datos.telefono);
119 fclose(fp);
120 }
121
122 TipoAgenda lee_agenda(char nombre_fichero[])
123 {
124 TipoAgenda agenda;
125 struct Entrada * entrada_leida;
126 FILE * fp;
127 char nombre[MAXCADENA+1], direccion[MAXCADENA+1], telefono[MAXCADENA+1];
128 int longitud;
129
130 agenda = crea_agenda();
131
132 fp = fopen(nombre_fichero, "r");
133 while (1) {
134 fgets(nombre, MAXCADENA, fp);
135 if (feof (fp)) break; // Si se acab´o el fichero, acabar la lectura.
136 quita_fin_de_linea(nombre);
137
138 fgets(direccion, MAXCADENA, fp);
139 quita_fin_de_linea(direccion);
140
141 fgets(telefono, MAXCADENA, fp);
142 quita_fin_de_linea(telefono);
143
144 agenda = anyadir_entrada(agenda, nombre, direccion, telefono);
145 }
146 fclose(fp);
147
148 return agenda;
149 }
150
151
152 /************************************************************************
153 * Programa principal
154 ************************************************************************/
156
157 int main(void)
158 {
159 TipoAgenda miagenda;
160 struct NodoAgenda * encontrada;
Introducci´on a la Programaci´on con C 333
5.2 Ficheros de texto
161 int opcion;
162 char nombre[MAXCADENA+1];
163 char direccion[MAXCADENA+1];
164 char telefono[MAXCADENA+1];
165 char linea[MAXCADENA+1];
166
167 miagenda = lee_agenda("agenda.txt");
168
169 do {
170 printf ("Men´u:n");
171 printf ("1) Ver contenido completo de la agenda.n");
172 printf ("2) Dar de alta una persona.n");
173 printf ("3) Buscar tel´efonos de una persona.n");
174 printf ("4) Salir.n");
175 printf ("Opci´on: ");
176 gets(linea); sscanf (linea, "%d", &opcion);
177
178 switch(opcion) {
179
180 case Ver:
181 muestra_agenda(miagenda);
182 break;
183
184 case Alta:
185 printf ("Nombre : "); gets(nombre);
186 printf ("Direcci´on: "); gets(direccion);
187 printf ("Tel´efono : "); gets(telefono);
188 miagenda = anyadir_entrada(miagenda, nombre, direccion, telefono);
189 break;
190
191 case Buscar:
192 printf ("Nombre: "); gets(nombre);
193 encontrada = buscar_entrada_por_nombre(miagenda, nombre);
194 if (encontrada == NULL)
195 printf ("No hay nadie llamado %s en la agenda.n", nombre);
196 else
197 muestra_entrada(encontrada);
198 break;
199 }
200 } while (opcion != Salir);
201
202
203 escribe_agenda(miagenda, "agenda.txt");
204 libera_agenda(miagenda);
205
206 return 0;
207 }
Entrada/salida de fichero para el programa de gesti´on de una colecci´on de discos
Acabamos esta secci´on dedicada a los ficheros de texto con una aplicaci´on pr´actica. Vamos a
a˜nadir funcionalidad al programa desarrollado en el apartado 4.11: el programa cargar´a la ((base
de datos)) tan pronto inicie su ejecuci´on leyendo un fichero de texto y la guardar´a en el mismo
fichero, recogiendo los cambios efectuados, al final.
En primer lugar, discutamos brevemente acerca del formato del fichero de texto. Podemos
almacenar cada dato en una l´ınea, as´ı:
discoteca.txt
1 Expression
2 John Coltrane
3 1972
4 Ogunde
5 To be
334 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros
6 Offering
7 Expression
8 Number One
9 Logos
10 Tangerine Dream
11 1982
12 Logos
13 Dominion
14 Ignacio
15 Vangelis
16 1977
17 Ignacio
Pero hay un serio problema: ¿c´omo sabe el programa d´onde empieza y acaba cada disco? El
programa no puede distinguir entre el t´ıtulo de una canci´on, el de un disco o el nombre de un
int´erprete. Podr´ıamos marcar cada l´ınea con un par de caracteres que nos indiquen qu´e tipo de
informaci´on mantiene:
discoteca.txt
1 TD Expression
2 IN John Coltrane
3 A~N 1972
4 TC Ogunde
5 TC To be
6 TC Offering
7 TC Expression
8 TC Number One
9 TD Logos
10 IN Tangerine Dream
11 A~N 1982
12 TC Logos
13 TC Dominion
14 TD Ignacio
15 IN Vangelis
16 A~N 1977
17 TC Ignacio
Con TD indicamos ((t´ıtulo de disco)); con IN, ((int´erprete)); con A~N, ((a˜no)); y con TC, ((t´ıtulo de
canci´on)). Pero esta soluci´on complica las cosas en el programa: no sabemos de qu´e tipo es una
l´ınea hasta haber le´ıdo sus dos primeros caracteres. O sea, sabemos que un disco ((ha acabado))
cuando ya hemos le´ıdo una l´ınea del siguiente. No es que no se pueda trabajar as´ı, pero resulta
complicado. Como podemos definir libremente el formato, optaremos por uno que preceda los
t´ıtulos de las canciones por un n´umero que indique cu´antas canciones hay:
discoteca.txt discoteca.txt
1 Expression
2 John Coltrane
3 1972
4 5
5 Ogunde
6 To be
7 Offering
8 Expression
9 Number One
10 Logos
11 Tangerine Dream
12 1982
13 2
14 Logos
15 Dominion
16 Ignacio
17 Vangelis
18 1977
Introducci´on a la Programaci´on con C 335
5.2 Ficheros de texto
19 1
20 Ignacio
La lectura de la base de datos es relativamente sencilla:
1 void quita_fin_de_linea(char linea[])
2 {
3 int i;
4 for (i=0; linea[i] != ’0’; i++)
5 if (linea[i] == ’n’) {
6 linea[i] = ’0’;
7 break;
8 }
9 }
10
11 TipoColeccion carga_coleccion(char nombre_fichero[])
12 {
13 FILE * f;
14 char titulo_disco[MAXCAD+1], titulo_cancion[MAXCAD+1], interprete[MAXCAD+1];
15 char linea[MAXCAD+1];
16 int anyo;
17 int numcanciones;
18 int i;
19 TipoColeccion coleccion;
20 TipoListaCanciones lista_canciones;
21
22 coleccion = crea_coleccion();
23 f = fopen(nombre_fichero, "r");
24 while(1) {
25 fgets(titulo_disco, MAXCAD, f);
26 if (feof (f))
27 break;
28 quita_fin_de_linea(titulo_disco);
29 fgets(interprete, MAXCAD, f);
30 quita_fin_de_linea(interprete);
31 fgets(linea, MAXCAD, f); sscanf (linea, "%d", &anyo);
32 fgets(linea, MAXCAD, f); sscanf (linea, "%d", &numcanciones);
33 lista_canciones = crea_lista_canciones();
34 for (i=0; i<numcanciones; i++) {
35 fgets(titulo_cancion, MAXCAD, f);
36 quita_fin_de_linea(titulo_cancion);
37 lista_canciones = anyade_cancion(lista_canciones, titulo_cancion);
38 }
39 coleccion = anyade_disco(coleccion, titulo_disco, interprete, anyo, lista_canciones);
40 }
41 fclose(f);
42
43 return coleccion;
44 }
Tan s´olo cabe rese˜nar dos cuestiones:
La detecci´on del final de fichero se ha de hacer tras una lectura infructuosa, por lo que la
hemos dispuesto tras el primer fgets del bucle.
La lectura de l´ıneas con fgets hace que el salto de l´ınea est´e presente, as´ı que hay que
eliminarlo expl´ıcitamente.
Al guardar el fichero hemos de asegurarnos de que escribimos la informaci´on en el mismo
formato:
1 void guarda_coleccion(TipoColeccion coleccion, char nombre_fichero[])
2 {
3 struct Disco * disco;
336 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros
4 struct Cancion * cancion;
5 int numcanciones;
6 FILE * f;
7
8 f = fopen(nombre_fichero, "w");
9 for (disco = coleccion; disco != NULL; disco = disco->sig) {
10 fprintf (f, "%sn", disco->titulo);
11 fprintf (f, "%sn", disco->interprete);
12 fprintf (f, "%dn", disco->anyo);
13
14 numcanciones = 0;
15 for (cancion = disco->canciones; cancion != NULL; cancion = cancion->sig)
16 numcanciones++;
17 fprintf (f, "%dn", numcanciones);
18
19 for (cancion = disco->canciones; cancion != NULL; cancion = cancion->sig)
20 fprintf (f, "%sn", cancion->titulo);
21 }
22 fclose(f);
23 }
Observa que hemos recorrido dos veces la lista de canciones de cada disco: una para saber
cu´antas canciones contiene (y as´ı poder escribir en el fichero esa cantidad) y otra para escribir
los t´ıtulos de las canciones.
Aqu´ı tienes las modificaciones hechas al programa principal:
discoteca2 1.c discoteca2.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <ctype.h>
.
.
.
253
254
255 int main(void)
256 {
257 int opcion;
258 TipoColeccion coleccion;
259 char titulo_disco[MAXCAD+1], titulo_cancion[MAXCAD+1], interprete[MAXCAD+1];
260 char linea[MAXCAD+1];
261 int anyo;
262 struct Disco * undisco;
263 TipoListaCanciones lista_canciones;
264
265 coleccion = carga_coleccion("discoteca.txt");
266
267 do {
268 printf ("Men´un");
269 printf ("1) A~nadir discon");
270 printf ("2) Buscar por t´ıtulo del discon");
271 printf ("3) Buscar por int´erpreten");
272 printf ("4) Buscar por t´ıtulo de canci´onn");
273 printf ("5) Mostrar todon");
274 printf ("6) Eliminar un disco por t´ıtulo e int´erpreten");
275 printf ("7) Finalizarn");
276 printf ("Opci´on: "); gets(linea); sscanf (linea, "%d", &opcion);
.
.
.
331
332 guarda_coleccion(coleccion, "discoteca.txt");
333 coleccion = libera_coleccion(coleccion);
334
335 return 0;
336 }
Introducci´on a la Programaci´on con C 337
5.2 Ficheros de texto
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 319 La gesti´on de ficheros mediante su carga previa en memoria puede resultar problem´atica
al trabajar con grandes vol´umenes de informaci´on. Modifica el programa de la agenda para que
no cargue los datos en memoria. Todas las operaciones (a˜nadir datos y consultar) se efectuar´an
gestionando directamente ficheros.
· 320 Modifica el programa propuesto en el ejercicio anterior para que sea posible borrar
entradas de la agenda. (Una posible soluci´on pasa por trabajar con dos ficheros, uno original
y uno para copias, de modo que borrar una informaci´on sea equivalente a no escribirla en la
copia.)
· 321 Modifica el programa de la agenda para que se pueda mantener m´as de un tel´efono
asociado a una persona. El formato del fichero pasa a ser el siguiente:
Una l´ınea que empieza por la letra N contiene el nombre de una persona.
Una l´ınea que empieza por la letra D contiene la direcci´on de la persona cuyo nombre
acaba de aparecer.
Una l´ınea que empieza por la letra T contiene un n´umero de tel´efono asociado a la persona
cuyo nombre apareci´o m´as recientemente en el fichero.
Ten en cuenta que no se puede asociar m´as de una direcci´on a una persona (y si eso ocurre en
el fichero, debes notificar la existencia de un error), pero s´ı m´as de un tel´efono. Adem´as, puede
haber l´ıneas en blanco (o formadas ´unicamente por espacios en blanco) en el fichero. He aqu´ı
un ejemplo de fichero con el nuevo formato:
agenda.txt
1 N Juan Gil
2 D Ronda Mijares, 1220
3 T 964 123456
4
5 N Ana Garc´ıa
6 D Plaza del Sol, 13
7 T 964-872777
8 T 964-872778
9
10
11 N Pepe P´erez
12 D Calle de Arriba, 1
13 T 964 263 263
14 T 964 163 163
15 T 96 2663 663
· 322 En un fichero matriz.mat almacenamos los datos de una matriz de enteros con el
siguiente formato:
La primera l´ınea contiene el n´umero de filas y columnas.
Cada una de las restantes l´ıneas contiene tantos enteros (separados por espacios) como
indica el n´umero de columnas. Hay tantas l´ıneas de este estilo como filas tiene la matriz.
Este ejemplo define una matriz de 3 × 4 con el formato indicado:
matriz.txt
1 3 4
2 1 0 3 4
3 0 -1 12 -1
4 3 0 99 -3
Escribe un programa que lea matriz.mat efectuando las reservas de memoria din´amica que
corresponda y muestre por pantalla, una vez cerrado el fichero, el contenido de la matriz.
338 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros
· 323 Modifica el programa del ejercicio anterior para que, si hay menos l´ıneas con valores de
filas que filas declaradas en la primera l´ınea, se rellene el restante n´umero de filas con valores
nulos.
Aqu´ı tienes un ejemplo de fichero con menos filas que las declaradas:
matriz incompleta.txt
1 3 4
2 1 0 3 4
· 324 Dise˜na un programa que facilite la gesti´on de una biblioteca. El programa permitir´a
prestar libros. De cada libro se registrar´a al menos el t´ıtulo y el autor. En cualquier instante se
podr´a volcar el estado de la biblioteca a un fichero y cargarlo de ´el.
Conviene que la biblioteca sea una lista de nodos, cada uno de los cuales representa un
libro. Uno de los campos del libro podr´ıa ser una cadena con el nombre del prestatario. Si dicho
nombre es la cadena vac´ıa, se entender´a que el libro est´a disponible.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Permisos Unix
Los ficheros Unix llevan asociados unos permisos con los que es posible determinar qu´e
usuarios pueden efectuar qu´e acciones sobre cada fichero. Las acciones son: leer, escribir y
ejecutar (esta ´ultima limitada a ficheros ejecutables, es decir, resultantes de una compilaci´on
o que contienen c´odigo fuente de un lenguaje interpretado y siguen cierto convenio). Se
puede fijar cada permiso para el usuario ((propietario)) del fichero, para los usuarios de su
mismo grupo o para todos los usuarios del sistema.
Cuando ejecutamos el comando ls con la opci´on -l, podemos ver los permisos codifi-
cados con las letras rwx y el car´acter -:
-rw-r--r-- 1 usuario migrupo 336 may 12 10:43 kk.c
-rwxr-x--- 1 usuario migrupo 13976 may 12 10:43 a.out
El fichero kk.c tiene permiso de lectura y escritura para el usuario (caracteres 2 a 4), de
s´olo lectura para los usuarios de su grupo (caracteres 5 a 7) y de s´olo lectura para el resto
de usuarios (caracteres 8 a 10). El fichero a.out puede ser le´ıdo, modificado y ejecutado
por el usuario. Los usuarios del mismo grupo pueden leerlo y ejecutarlo, pero no modificar
su contenido. El resto de usuarios no puede acceder al fichero.
El comando Unix chmod permite modificar los permisos de un fichero. Una forma tradi-
cional de hacerlo es con un n´umero octal que codifica los permisos. Aqu´ı tienes un ejemplo
de uso:
$ chown 0700 a.out
$ ls -l a.out
-rwx------ 1 usuario migrupo 13976 may 12 10:43 a.out
El valor octal 0700 (que en binario es 111000000), por ejemplo, otorga permisos de
lectura, escritura y ejecuci´on al propietario del fichero, y elimina cualquier permiso para el
resto de usuarios. De cada 3 bits, el primero fija el permiso de lectura, el segundo el de
escritura y el tercero el de ejecuci´on. Los 3 primeros bits corresponden al usuario, los tres
siguientes al grupo y los ´ultimos 3 al resto. As´ı pues, 0700 equivale a -rwx------ en la
notaci´on de ls -l.
Por ejemplo, para que a.out sea tambi´en legible y ejecutable por parte de cualquier
miembro del grupo del propietario puedes usar el valor 0750 (que equivale a -rwxr-x---).
5.2.3. Los ((ficheros)) de consola
Hay tres ficheros de texto predefinidos y ya abiertos cuando se inicia un programa: los ((ficheros))
de consola. En realidad, no son ficheros, sino dispositivos:
stdin (entrada est´andar): el teclado;
stdout (salida est´andar): la pantalla;
Introducci´on a la Programaci´on con C 339
5.2 Ficheros de texto
stderr (salida est´andar de error): ¿?
¿Qu´e es stderr? En principio es tambi´en la pantalla, pero podr´ıa ser, por ejemplo un fichero en
el que deseamos llevar un cuaderno de bit´acora con las anomal´ıas o errores detectados durante
la ejecuci´on del programa.
La funci´on printf es una forma abreviada de llamar a fprintf sobre stdout y scanf encubre
una llamada a fscanf sobre stdin. Por ejemplo, estas dos llamadas son equivalentes:
printf ("Esto es la %sn", "pantalla");
f printf (stdout, "Esto es la %sn", "pantalla");
El hecho de que, en el fondo, Unix considere al teclado y la pantalla equivalentes a ficheros
nos permite hacer ciertas cosas curiosas. Por ejemplo, si deseamos ejecutar un programa cuyos
datos se deben leer de teclado o de fichero, seg´un convenga, podemos decidir la fuente de
entrada en el momento de la ejecuci´on del programa. Este programa, por ejemplo, permite
elegir al usuario entre leer de teclado o leer de fichero:
selecciona entrada.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE * fp;
6 char dedonde[80], nombre[80];
7 int n;
8
9 printf ("Leo de fichero o de teclado (f/t)?: ");
10 gets(dedonde);
11 if (dedonde[0] == ’f’) {
12 printf ("Nombre del fichero: ");
13 gets(nombre);
14 fp = fopen(nombre, "r");
15 }
16 else
17 fp = stdin;
18
19 ...
20 fscanf (fp, "%d", &n); /* Lee de fichero o teclado. */
21 ...
22 if (fp != stdin)
23 fclose(fp);
24 ...
25
26 return 0;
27 }
Existe otra forma de trabajar con fichero o teclado que es m´as c´omoda para el programador:
usando la capacidad de redirecci´on que facilita el int´erprete de comandos Unix. La idea consiste
en desarrollar el programa considerando s´olo la lectura por teclado y, cuando iniciamos la ejecu-
ci´on del programa, redirigir un fichero al teclado. Ahora ver´as c´omo. F´ıjate en este programa:
pares.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i, n;
6
7 for (i=0; i<10; i++) {
8 scanf ("%d", &n);
9 if (n%2==0)
10 printf ("[%d]", n);
11 }
340 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros
De cadena a entero o flotante
Los ficheros de texto contienen eso, texto. No obstante, el texto se interpreta en ocasiones
como si codificara valores enteros o flotantes. La funci´on fscanf , por ejemplo, es capaz
de leer texto de un fichero e interpretarlo como si fuera un entero o un flotante. Cuando
hacemos fscanf (f, "%d", &a), donde a es de tipo int, se leen caracteres del fichero y se
interpretan como un entero. Pero hay un problema potencial: el texto puede no corresponder
a un valor entero, con lo que la lectura no se efectuar´ıa correctamente. Una forma de curarse
en salud es leer como cadena los siguientes caracteres (con fscanf y la marca de formato %s
o con gets, por ejemplo), comprobar que la secuencia de caracteres le´ıda describe un entero
(o un flotante, seg´un convenga) y convertir ese texto en un entero (o flotante). ¿C´omo
efectuar la conversi´on? C nos ofrece en su biblioteca est´andar la funci´on atoi, que recibe
una cadena y devuelve un entero. Has de incluir la cabecera stdlib.h para usarla. Aqu´ı
tienes un ejemplo de uso:
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int main(void)
5 {
6 char a[] = "123";
7 int b;
8
9 b = atoi(a);
10
11 printf ("La cadena %s se interpreta como el entero %d con atoin", a, b);
12
13 return 0;
14 }
Si deseas interpretar el texto como un float, puedes usar atof en lugar de atoi. As´ı de f´acil.
12
13 return 0;
14 }
Si lo compilas para generar un programa pares, lo ejecutas e introduces los siguientes 10 n´umeros
enteros, obtendr´as este resultado en pantalla:
$ pares
3
5
6
[6]
7
2
[2]
10
[10]
2
[2]
1
3
13
Cada vez que el ordenador ha detectado un n´umero par, lo ha mostrado en pantalla entre
corchetes.
Creemos ahora, con la ayuda de un editor de texto, numeros.txt, un fichero de texto con
los mismos 10 n´umeros enteros que hemos introducido por teclado antes:
numeros.txt
1 3
Introducci´on a la Programaci´on con C 341
5.2 Ficheros de texto
2 5
3 6
4 7
5 2
6 10
7 2
8 1
9 3
10 13
Podemos llamar a pares as´ı:
$ pares < numeros.txt
[6]
[2]
[10]
[2]
El car´acter < indica a Unix que lea del fichero numeros.txt en lugar de leer del teclado. El
programa, sin tocar una sola l´ınea, pasa a leer los valores de numeros.txt y muestra por pantalla
los que son pares.
Tambi´en podemos redirigir la salida (la pantalla) a un fichero. F´ıjate:
$ pares < numeros.txt > solopares.txt
Ahora el programa se ejecuta sin mostrar texto alguno por pantalla y el fichero solopares.txt
acaba conteniendo lo que debiera haberse mostrado por pantalla.
$ cat solopares.txt
[6]
[2]
[10]
[2]
Para redirigir la salida de errores, puedes usar el par de caracteres 2> seguido del nombre
del fichero en el que se escribir´an los mensajes de error.
La capacidad de redirigir los dispositivos de entrada, salida y errores tiene infinidad de apli-
caciones. Una evidente es automatizar la fase de pruebas de un programa durante su desarrollo.
En lugar de escribir cada vez todos los datos que solicita un programa para ver si efect´ua correc-
tamente los c´alculos, puedes preparar un fichero con los datos de entrada y utilizar redirecci´on
para que el programa los lea autom´aticamente.
5.2.4. Un par de utilidades
Hemos aprendido a crear ficheros y a modificar su contenido. No sabemos, sin embargo, c´omo
eliminar un fichero del sistema de ficheros ni c´omo rebautizarlo. Hay dos funciones de la librer´ıa
est´andar de C (accesibles al incluir stdio.h) que permiten efectuar estas dos operaciones:
remove: elimina el fichero cuya ruta se proporciona.
int remove(char ruta[]);
La funci´on devuelve 0 si se consigui´o eliminar el fichero y otro valor si se cometi´o alg´un
error. ¡Ojo! No confundas borrar un fichero con borrar el contenido de un fichero. La
funci´on remove elimina completamente el fichero. Abrir un fichero en modo escritura y
cerrarlo inmediatamente elimina su contenido, pero el fichero sigue existiendo (ocupando,
eso s´ı, 0 bytes).
rename: cambia el nombre de un fichero.
int rename(char ruta_original[], char nueva_ruta[]);
La funci´on devuelve 0 si no hubo error, y otro valor en caso contrario.
342 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros
Los peligros de gets. . . y c´omo superarlos
Ya habr´as podido comprobar que gets no es una funci´on segura, pues siempre es posible
desbordar la memoria reservada leyendo una cadena suficientemente larga. Algunos com-
piladores generan una advertencia cuando detectan el uso de gets. ¿C´omo leer, pues, una
l´ınea de forma segura? Una posibilidad consiste en escribir nuestra propia funci´on de lectura
car´acter a car´acter (con ayuda de la funci´on fgetc) e imponer una limitaci´on al n´umero de
caracteres le´ıdos.
1 int lee_linea(char linea[], int max_lon)
2 {
3 int c, nc = 0;
4 max_lon--; /* Se reserva un car´acter para el 0 */
5
6 while ( (c = fgetc(stdin)) != EOF ) {
7 if (c == ’n’)
8 break;
9 if (nc < max_lon)
10 linea[nc++] = c;
11 }
12
13 if (c == EOF && nc == 0)
14 return EOF;
15
16 linea[nc] = ’0’;
17 return nc;
18 }
Para leer una cadena en un vector de caracteres con una capacidad m´axima de 100
caracteres, haremos:
lee_linea(cadena, 100);
El valor de cadena se modificar´a para contener la cadena le´ıda. La cadena m´as larga le´ıda
tendr´a una longitud de 99 caracteres (recuerda que el ’0’ ocupa uno de los 100).
Pero hay una posibilidad a´un m´as sencilla: usar fgets sobre stdin:
fgets(cadena, 100, stdin);
Una salvedad: fgets incorpora a la cadena le´ıda el salto de l´ınea, cosa que gets no hace.
La primera versi´on, no obstante, sigue teniendo inter´es, pues te muestra un ((esqueleto))
de funci´on ´util para un control detallado de la lectura por teclado. Inspir´andote en ella
puedes escribir, por ejemplo, una funci´on que s´olo lea d´ıgitos, o letras, o texto que satisface
alguna determinada restricci´on.
La consulta de teclado
La funci´on getc (o, para el caso, fgetc actuando sobre stdin) bloquea la ejecuci´on del
programa hasta que el usuario teclea algo y pulsa la tecla de retorno. Muchos programadores
se preguntan ¿c´omo puedo saber si una tecla est´a pulsada o no sin quedar bloqueado? Ciertas
aplicaciones, como los videojuegos, necesitan efectuar consultas al estado del teclado no
bloqueantes. Malas noticias: no es un asunto del lenguaje C, sino de bibliotecas espec´ıficas.
El C est´andar nada dice acerca de c´omo efectuar esa operaci´on.
En Unix, la biblioteca curses, por ejemplo, permite manipular los terminales y acceder de
diferentes modos al teclado. Pero no es una biblioteca f´acil de (aprender a) usar. Y, adem´as,
presenta problemas de portabilidad, pues no necesariamente est´a disponible en todos los
sistemas operativos.
Cosa parecida podemos decir de otras cuestiones: sonido, gr´aficos tridimensionales, in-
terfaces gr´aficas de usuario, etc. C, en tanto que lenguaje de programaci´on estandarizado,
no ofrece soporte. Eso s´ı: hay bibliotecas para infinidad de campos de aplicaci´on. Tendr´as
que encontrar la que mejor se ajusta a tus necesidades y. . . ¡estudiar!
Introducci´on a la Programaci´on con C 343
5.3 Ficheros binarios
5.3. Ficheros binarios
5.3.1. Abrir, leer/escribir, cerrar
La gesti´on de ficheros binarios obliga a trabajar con el mismo protocolo b´asico:
1. abrir el fichero en el modo adecuado,
2. leer y/o escribir informaci´on,
3. y cerrar el fichero.
La funci´on de apertura de un fichero binario es la misma que hemos usado para los ficheros
de texto: fopen. Lo que cambia es el modo de apertura: debe contener la letra b. Los modos de
apertura b´asicos3
para ficheros binarios son, pues:
"rb" (lectura): El primer byte le´ıdo es el primero del fichero.
"wb" (escritura): Trunca el fichero a longitud 0. Si el fichero no existe, se crea.
"ab" (adici´on): Es un modo de escritura que preserva el contenido original del fichero.
Los datos escritos se a˜naden al final del fichero.
Si el fichero no puede abrirse por cualquier raz´on, fopen devuelve el valor NULL.
La funci´on de cierre del fichero es fclose.
Las funciones de lectura y escritura s´ı son diferentes:
fread: recibe una direcci´on de memoria, el n´umero de bytes que ocupa un dato, el n´umero
de datos a leer y un fichero. He aqu´ı su prototipo4
:
int fread( void * direccion, int tam, int numdatos, FILE * fichero );
Los bytes le´ıdos se almacenan a partir de direccion. Devuelve el n´umero de datos que ha
conseguido leer (y si ese valor es menor que numdatos, es porque hemos llegado al final
del fichero y no se ha podido efectuar la lectura completa).
fwrite: recibe una direcci´on de memoria, el n´umero de bytes que ocupa un dato, el n´umero
de datos a escribir y un fichero. Este es su prototipo:
int fwrite( void * direccion, int tam, int numdatos, FILE * fichero );
Escribe en el fichero los tam por numdatos bytes existentes desde direccion en adelante.
Devuelve el n´umero de datos que ha conseguido escribir (si vale menos que numdatos,
hubo alg´un error de escritura).
Empezaremos a comprender c´omo trabajan estas funciones con un sencillo ejemplo. Vamos
a escribir los diez primeros n´umeros enteros en un fichero:
diez enteros.c diez enteros.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE * fp;
6 int i;
7
8 fp = fopen("primeros.dat", "wb");
9 for (i=0; i<10; i++)
10 fwrite(&i, sizeof(int), 1, fp);
11 fclose(fp);
12
13 return 0;
14 }
3M´as adelante te presentamos tres modos de apertura adicionales.
4Bueno, casi. El prototipo no usa el tipo int, sino size t, que est´a definido como unsigned int. Preferimos
presentarte una versi´on modificada del prototipo para evitar introducir nuevos conceptos.
344 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros
Analicemos la llamada a fwrite. F´ıjate: pasamos la direcci´on de memoria en la que empieza
un entero (con &i) junto al tama˜no en bytes de un entero (sizeof(int), que vale 4) y el valor
1. Estamos indicando que se van a escribir los 4 bytes (resultado de multiplicar 1 por 4) que
empiezan en la direcci´on &i, es decir, se va a guardar en el fichero una copia exacta del contenido
de i.
Quiz´a entiendas mejor qu´e ocurre con esta otra versi´on capaz de escribir un vector completo
en una sola llamada a fwrite:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE * fp;
6 int i, v[10];
7
8 for (i=0; i<10; i++)
9 v[i] = i;
10 fp = fopen("primeros.dat", "wb");
11 fwrite(v, sizeof(int), 10, fp);
12 fclose(fp);
13
14 return 0;
15 }
Ahora estamos pasando la direcci´on en la que empieza un vector (v es una direcci´on, as´ı que no
hemos de poner un & delante), el tama˜no de un elemento del vector (sizeof(int)) y el n´umero
de elementos del vector (10). El efecto es que se escriben en el fichero los 40 bytes de memoria
que empiezan donde empieza v. Resultado: todo el vector se almacena en disco con una sola
operaci´on de escritura. C´omodo, ¿no?
Ya te dijimos que la informaci´on de todo fichero binario ocupa exactamente el mismo n´umero
de bytes que ocupar´ıa en memoria. Hagamos la prueba. Veamos con ls -l, desde el int´erprete
de comandos de Unix, cu´anto ocupa el fichero:
$ ls -l primeros.dat
-rw-r--r-- 1 usuario migrupo 40 may 10 11:00 primeros.dat
Efectivamente, ocupa exactamente 40 bytes (el n´umero que aparece en quinto lugar). Si lo
mostramos con cat, no sale nada con sentido en pantalla.
$ cat primeros.dat
$
¿Por qu´e? Porque cat interpreta el fichero como si fuera de texto, as´ı que encuentra la siguiente
secuencia binaria:
1 00000000 00000000 00000000 00000000
2 00000000 00000000 00000000 00000001
3 00000000 00000000 00000000 00000010
4 00000000 00000000 00000000 00000011
5 00000000 00000000 00000000 00000100
6 ...
Los valores ASCII de cada grupo de 8 bits no siempre corresponden a caracteres visibles, por
lo que no se representan como s´ımbolos en pantalla (no obstante, algunos bytes s´ı tienen efecto
en pantalla; por ejemplo, el valor 9 corresponde en ASCII al tabulador).
Hay una herramienta Unix que te permite inspeccionar un fichero binario: od (abreviatura
de ((octal dump)), es decir, ((volcado octal))).
$ od -l primeros.dat
0000000 0 1 2 3
0000020 4 5 6 7
0000040 8 9
0000050
Introducci´on a la Programaci´on con C 345
5.3 Ficheros binarios
(La opci´on -l de od hace que muestre la interpretaci´on como enteros de grupos de 4 bytes.)
¡Ah´ı est´an los n´umeros! La primera columna indica (en hexadecimal) el n´umero de byte del
primer elemento de la fila.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 325 ¿Qu´e aparecer´a en pantalla si mostramos con el comando cat el contenido del fichero
binario otraprueba.dat generado en este programa?:
otra prueba.c otra prueba.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE * fp;
6 int i, v[26];
7
8 fp = fopen("otra prueba.dat", "wb");
9 for (i=97; i<123; i++)
10 v[i-97] = i;
11 fwrite(v, sizeof(int), 26, fp);
12 fclose(fp);
13
14 return 0;
15 }
(Una pista: el valor ASCII del car´acter ’a’ es 97.)
¿Y qu´e aparecer´a si lo visualizas con el comando od -c (la opci´on -c indica que se desea
ver el fichero car´acter a car´acter e interpretado como secuencia de caracteres).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Ya puedes imaginar c´omo se leen datos de un fichero binario: pasando la direcci´on de me-
moria en la que queremos que se copie cierta cantidad de bytes del fichero. Los dos programas
siguientes, por ejemplo, leen los diez valores escritos en los dos ´ultimos programas. El primero
lee entero a entero (de 4 bytes en 4 bytes), y el segundo con una sola operaci´on de lectura
(cargando los 40 bytes de golpe):
lee primeros.c lee primeros.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE * fp;
6 int i, n;
7
8 fp = fopen("primeros.dat", "rb");
9 for (i=0; i<10; i++) {
10 fread(&n, sizeof(int), 1, fp);
11 printf ("%dn", n);
12 }
13 fclose(fp);
14
15 return 0;
16 }
lee primeros2.c lee primeros2.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE * fd;
6 int i, v[10];
7
8 fp = fopen("primeros.dat", "rb");
346 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros
9 fread(v, sizeof(int), 10, fp);
10 for (i=0; i<10; i++)
11 printf ("%dn", v[i]);
12 fclose(fp);
13
14 return 0;
15 }
En los dos programas hemos indicado expl´ıcitamente que ´ıbamos a leer 10 enteros, pues
sab´ıamos de antemano que hab´ıa exactamente 10 n´umeros en el fichero. Es f´acil modificar el
primer programa para que lea tantos enteros como haya, sin conocer a priori su n´umero:
lee todos.c lee todos.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE * fp;
6 int n;
7
8 fp = fopen("primeros.dat", "rb");
9 fread(&n, sizeof(int), 1, fp);
10 while (!feof (fp)) {
11 printf ("%dn", n);
12 fread(&n, sizeof(int), 1, fp);
13 }
14 fclose(fp);
15
16 return 0;
17 }
Lo cierto es que hay una forma m´as idiom´atica, m´as com´un en C de expresar lo mismo:
lee todos2.c lee todos2.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE * fp;
6 int n;
7
8 f = fopen("primeros.dat", "rb");
9 while ( fread(&n, sizeof(int), 1, fp) == 1 )
10 printf ("%dn", n);
11 fclose(fp);
12
13 return 0;
14 }
En esta ´ultima versi´on, la lectura de cada entero se efect´ua con una llamada a fread en la
condici´on del while.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 326 Dise˜na un programa que genere un fichero binario primos.dat con los 1000 primeros
n´umeros primos.
· 327 Dise˜na un programa que a˜nada al fichero binario primos.dat (ver ejercicio anterior) los
100 siguientes n´umeros primos. El programa leer´a el contenido actual del fichero para averiguar
cu´al es el ´ultimo primo conocido. A continuaci´on, abrir´a el fichero en modo adici´on y a˜nadir´a
100 nuevos primos. Si ejecut´asemos dos veces el programa, el fichero acabar´ıa conteniendo los
1200 primeros primos.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
No s´olo puedes guardar tipos relativamente elementales. Tambi´en puedes almacenar en disco
tipos de datos creados por ti. Este programa, por ejemplo, lee de disco un vector de puntos, lo
modifica y escribe en el fichero el contenido del vector:
Introducci´on a la Programaci´on con C 347
5.3 Ficheros binarios
escribe registro.c escribe registro.c
1 #include <stdio.h>
2 #include <math.h>
3
4 struct Punto {
5 float x;
6 float y;
7 };
8
9 int main(void)
10 {
11 FILE * fp;
12 struct Punto v[10];
13 int i;
14
15 // Cargamos en memoria un vector de puntos.
16 fp = fopen("puntos.dat", "rb");
17 fread(v, sizeof(struct Punto), 10, fp);
18 fclose(fp);
19
20 // Procesamos los puntos (calculamos el valor absoluto de cada coordenada).
21 for (i=0; i<10; i++) {
22 v[i].x = fabs(v[i].x);
23 v[i].y = fabs(v[i].y);
24 }
25
26 // Escribimos el resultado en otro fichero.
27 fp = fopen("puntos2.dat", "wb");
28 fwrite(v, sizeof(struct Punto), 10, fp);
29 fclose(fp);
30
31 return 0;
32 }
Esta otra versi´on no carga el contenido del primer fichero completamente en memoria en
una primera fase, sino que va leyendo, procesando y escribiendo punto a punto:
1 #include <stdio.h>
2 #include <math.h>
3
4 struct Punto {
5 float x;
6 float y;
7 };
8
9 int main(void)
10 {
11 FILE * fp_entrada, * fp_salida;
12 struct Punto p;
13 int i;
14
15 fp_entrada = fopen("puntos.dat", "rb");
16 fp_salida = fopen("puntos2.dat", "wb");
17
18 for (i=0; i<10; i++) {
19 fread(&p, sizeof(struct Punto), 1, fp_entrada);
20 p.x = fabs(p.x);
21 p.y = fabs(p.y);
22 fwrite(&p, sizeof(struct Punto), 1, fp_salida);
23 }
24 fclose(fp_entrada);
25 fclose(fp_salida);
26
27 return 0;
348 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros
28 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 328 Los dos programas anteriores suponen que hay diez puntos en el fichero puntos.dat.
Modif´ıcalos para que procesen tantos puntos como haya en el fichero.
· 329 Implementa un programa que genere un fichero llamado puntos.dat con 10 elementos
del tipo struct Punto. Las coordenadas de cada punto se generar´an aleatoriamente en el rango
[−10, 10]. Usa el ´ultimo programa para generar el fichero puntos2.dat. Comprueba que contiene
el valor absoluto de los valores de puntos.dat. Si es necesario, dise˜na un nuevo programa que
muestre por pantalla el contenido de un fichero de puntos cuyo nombre suministra por teclado
el usuario.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.3.2. Acceso directo
Los ficheros binarios pueden utilizarse como ((vectores en disco)) y acceder directamente a
cualquier elemento del mismo. Es decir, podemos abrir un fichero binario en modo ((lectura-
escritura)) y, gracias a la capacidad de desplazarnos libremente por ´el, leer/escribir cualquier
dato. Es como si dispusieras del control de avance r´apido hacia adelante y hacia atr´as de un
reproductor/grabador de cintas magnetof´onicas. Con ´el puedes ubicar el ((cabezal)) de lectu-
ra/escritura en cualquier punto de la cinta y pulsar el bot´on ((play)) para escuchar (leer) o el
bot´on ((record)) para grabar (escribir).
Adem´as de los modos de apertura de ficheros binarios que ya conoces, puedes usar tres
modos de lectura/escritura adicionales:
"r+b": No se borra el contenido del fichero, que debe existir previamente. El ((cabezal))
de lectura/escritura se sit´ua al principio del fichero.
"w+b": Si el fichero no existe, se crea, y si existe, se trunca el contenido a longitud cero.
El ((cabezal)) de lectura/escritura se sit´ua al principio del fichero.
"a+b": Si el fichero no existe, se crea. El ((cabezal)) de lectura/escritura se sit´ua al final
del fichero.
Para poder leer/escribir a voluntad en cualquier posici´on de un fichero abierto en alguno
de los modos binarios necesitar´as dos funciones auxiliares: una que te permita desplazarte a
un punto arbitrario del fichero y otra que te permita preguntar en qu´e posici´on te encuentras
en un instante dado. La primera de estas funciones es fseek, que desplaza el ((cabezal)) de
lectura/escritura al byte que indiquemos.
int fseek(FILE * fp, int desplazamiento, int desde_donde);
El valor desde_donde se fija con una constante predefinida que proporciona una interpretaci´on
distinta a desplazamiento:
SEEK_SET: el valor de desplazamiento es un valor absoluto a contar desde el principio del
fichero. Por ejemplo, fseek(fp, 3, SEEK_SET) desplaza al cuarto byte del fichero fp. (La
posici´on 0 corresponde al primer byte del fichero.)
SEEK_CUR: el valor de desplazamiento es un valor relativo al lugar en que nos encontramos
en un instante dado. Por ejemplo, si nos encontramos en el cuarto byte del fichero fp, la lla-
mada fseek(fp, -2, SEEK_CUR) nos desplazar´a al segundo byte, y fseek(fp, 2, SEEK_CUR)
al sexto.
SEEK_END: el valor de desplazamiento es un valor absoluto a contar desde el final del fichero.
Por ejemplo, fseek(fp, -1, SEEK_END) nos desplaza al ´ultimo byte de fp: si a continuaci´on
ley´esemos un valor, ser´ıa el del ´ultimo byte del fichero. La llamada fseek(fp, 0, SEEK_END)
nos situar´ıa fuera del fichero (en el mismo punto en el que estamos si abrimos el fichero
en modo de adici´on).
Introducci´on a la Programaci´on con C 349
5.3 Ficheros binarios
La funci´on devuelve el valor 0 si tiene ´exito, y un valor no nulo en caso contrario.
Has de tener siempre presente que los desplazamientos sobre el fichero se indican en bytes.
Si hemos almacenado enteros de tipo int en un fichero binario, deberemos tener la precauci´on
de que todos nuestros fseek tengan desplazamientos m´ultiplos de sizeof(int).
Este programa, por ejemplo, pone a cero todos los valores pares de un fichero binario de
enteros:
anula pares.c anula pares.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE * fp;
6 int n, bytes_leidos, cero = 0;
7
8 fp = fopen("fichero.dat", "r+b");
9 while (fread(&n, sizeof(int), 1, fp) != 0) {
10 if (n % 2 == 0) { // Si el ´ultimo valor le´ıdo es par...
11 fseek(fp, -sizeof(int), SEEK_CUR); // ... damos un paso atr´as ...
12 fwrite(&cero, sizeof(int), 1, fp); // ... y sobreescribimos su valor absoluto.
13 }
14 }
15 fclose(fp);
16
17 return 0;
18 }
La segunda funci´on que te presentamos en este apartado es ftell. Este es su prototipo:
int ftell(FILE *fp);
El valor devuelto por la funci´on es la posici´on en la que se encuentra el ((cabezal)) de lectu-
ra/escritura en el instante de la llamada.
Veamos un ejemplo. Este programa, por ejemplo, crea un fichero y nos dice el n´umero de
bytes del fichero:
cuenta bytes.c cuenta bytes.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE * fp;
6 int i, pos;
7
8 fp = fopen("prueba.dat", "wb");
9 for (i=0; i<10; i++)
10 fwrite(&i, sizeof(int), 1, fp);
11 fclose(fp);
12
13 fp = fopen("prueba.dat", "rb");
14 fseek(fp, 0, SEEK_END);
15 pos = ftell(fp);
16 printf ("Tama~no del fichero: %dn", pos);
17 fclose(fp);
18
19 return 0;
20 }
F´ıjate bien en el truco que permite conocer el tama˜no de un fichero: nos situamos al final del
fichero con ftell indicando que queremos ir al ((primer byte desde el final)) (byte 0 con el modo
SEEK_END) y averiguamos a continuaci´on la posici´on en la que nos encontramos (valor devuelto
por ftell).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 330 Dise˜na una funci´on de nombre rebobina que recibe un FILE * y nos ubica al inicio del
mismo.
350 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros
· 331 Dise˜na una funci´on que reciba un FILE * (ya abierto) y nos diga el n´umero de bytes
que ocupa. Al final, la funci´on debe dejar el cursor de lectura/escritura en el mismo lugar en el
que estaba cuando se la llam´o.
· 332 Dise˜na un programa que calcule y muestre por pantalla el m´aximo y el m´ınimo de los
valores de un fichero binario de enteros.
· 333 Dise˜na un programa que calcule el m´aximo de los enteros de un fichero binario y lo
intercambie por el que ocupa la ´ultima posici´on.
· 334 Nos pasan un fichero binario dobles.dat con una cantidad indeterminada de n´umeros
de tipo float. Sabemos, eso s´ı, que los n´umeros est´an ordenados de menor a mayor. Dise˜na un
programa que pida al usuario un n´umero y determine si est´a o no est´a en el fichero.
En una primera versi´on, implementa una b´usqueda secuencial que se detenga tan pronto
est´es seguro de que el n´umero buscado est´a o no. El programa, en su versi´on final, deber´a
efectuar la b´usqueda dicot´omicamente (en un cap´ıtulo anterior se ha explicado qu´e es una
b´usqueda dicot´omica).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Trabajar con ficheros binarios como si se tratara de vectores tiene ciertas ventajas, pero
tambi´en inconvenientes. La ventaja m´as obvia es la capacidad de trabajar con cantidades in-
gentes de datos sin tener que cargarlas completamente en memoria. El inconveniente m´as serio
es la enorme lentitud con que se pueden ejecutar entonces los programas. Ten en cuenta que
desplazarse por un fichero con fseek obliga a ubicar el ((cabezal)) de lectura/escritura del disco
duro, una operaci´on que es intr´ınsecamente lenta por comportar operaciones mec´anicas, y no
s´olo electr´onicas.
Si en un fichero binario mezclas valores de varios tipos resultar´a dif´ıcil, cuando no imposible,
utilizar sensatamente la funci´on fseek para posicionarse en un punto arbitrario del fichero.
Tenemos un problema similar cuando la informaci´on que guardamos en un fichero es de longitud
intr´ınsecamente variable. Pongamos por caso que usamos un fichero binario para almacenar una
lista de palabras. Cada palabra es de una longitud, as´ı que no hay forma de saber a priori en qu´e
byte del fichero empieza la n-´esima palabra de la lista. Un truco consiste en guardar cada palabra
ocupando tanto espacio como la palabra m´as larga. Este programa, por ejemplo, pide palabras
al usuario y las escribe en un fichero binario en el que todas las cadenas miden exactamente lo
mismo (aunque la longitud de cada una de ellas sea diferente):
guarda palabras.c guarda palabras.c
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char palabra[MAXLON+1], seguir[MAXLON+1];
8 FILE * fp;
9
10 fp = fopen("diccio.dat", "wb");
11 do {
12 printf ("Dame una palabra: "); gets(palabra);
13 fwrite(palabra, sizeof(char), MAXLON, fp);
14 printf ("Pulsa ’s’ para a~nadir otra."); gets(seguir);
15 } while (strcmp(seguir, "s") == 0);
16 fclose(fp);
17
18 return 0;
19 }
F´ıjate en que cada palabra ocupa siempre lo mismo, independientemente de su longitud:
80 bytes. Este otro programa es capaz ahora de mostrar la lista de palabras en orden inverso,
gracias a la ocupaci´on fija de cada palabra:
lee palabras orden inverso.c lee palabras orden inverso.c
1 #include <stdio.h>
2
3 #define MAXLON 80
Introducci´on a la Programaci´on con C 351
5.3 Ficheros binarios
Ficheros binarios en Python
Python tambi´en permite trabajar con ficheros binarios. La apertura, lectura/escritura y cierre
de ficheros se efect´ua con las funciones y m´etodos de Python que ya conoces: open, read,
write y close. Con read puedes leer un n´umero cualquiera de caracteres (de bytes) en una
cadena. Por ejemplo, f.read(4) lee 4 bytes del fichero f (previamente abierto con open). Si
esos 4 bytes corresponden a un entero (en binario), la cadena contiene 4 caracteres que lo
codifican (aunque no de forma que los podamos visualizar c´omodamente). ¿C´omo asignamos
a una variable el valor entero codificado en esa cadena? Python proporciona un m´odulo con
funciones que permiten pasar de binario a ((tipos Python)) y viceversa: el m´odulo struct. Su
funci´on unpack ((desempaqueta)) informaci´on binaria de una cadena. Para ((desempaquetar))
un entero de una cadena almacenada en una variable llamada enbinario la llamamos as´ı:
unpack("i", enbinario). El primer par´ametro desempe˜na la misma funci´on que las cadenas
de formato en scanf , s´olo que usa un juego de marcas de formato diferentes (i para el
equivalente a un int, d para float, q para long long, etc.. Consulta el manual del m´odulo
struct para conocerlos.). Aqu´ı tienes un ejemplo de uso: un programa que lee y muestra
los valores de un fichero binario de enteros:
1 from struct import unpack
2 f = open("primeros.dat", "r")
3 while 1:
4 c = f.read(4)
5 if c == ’’: break
6 v = unpack("i", c)
7 print v[0]
8 f.close()
F´ıjate en que el valor devuelto por unpack no es directamente el entero, sino una lista (en
realidad una tupla), por lo que es necesario indexarla para acceder al valor que nos interesa.
La raz´on de que devuelva una lista es que unpack puede desempaquetar varios valores a
la vez. Por ejemplo, unpack("iid", cadena) desempaqueta dos enteros y un flotante de
cadena (que debe tener al menos 16 bytes, claro est´a). Puedes asignar los valores devueltos
a tres variables as´ı: a, b, c = unpack("iid", cadena).
Hemos aprendido, pues, a leer ficheros binarios con Python. ¿C´omo los escribimos?
Siguiendo un proceso inverso: empaquetando primero nuestros ((valores Python)) en cadenas
que los codifican en binario mediante la funci´on pack y escribiendolas con el m´etodo write.
Este programa de ejemplo escribe un fichero binario con los n´umeros del 0 al 99:
1 from struct import pack
2 f = open("primeros.dat", "w")
3 for v in range(100):
4 c = pack("i", v)
5 f.write(c)
6 f.close()
S´olo queda que aprendas a implementar acceso directo a los ficheros binarios con Python.
Tienes disponibles los modos de apertura ’r+’, ’w+’ y ’a+’. Adem´as, el m´etodo seek
permite desplazarse a un byte cualquiera del fichero y el m´etodo tell indica en qu´e posici´on
del fichero nos encontramos.
4
5 int main(void)
6 {
7 FILE * fp;
8 char palabra[MAXLON+1];
9 int tam;
10
11 /* primero, averiguar el tama˜no del fichero (en palabras) */
12 fp = fopen("diccio.dat", "rb");
13 tam = fseek(fp, 0, SEEK_END) / MAXLON;
14
15 /* y ya podemos listarlas en orden inverso */
352 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros
16 for (i=tam-1; i>=0; i--) {
17 fseek(fp, i * MAXLON, SEEK_SET);
18 fread(palabra, sizeof(char), MAXLON, fp);
19 printf ("%sn", palabra);
20 }
21 fclose(fp);
22
23 return 0;
24 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 335 Los dos programas anteriores pueden plantear problemas cuando trabajan con palabras
que tienen 80 caracteres m´as el terminador. ¿Qu´e problemas? ¿C´omo los solucionar´ıas?
· 336 Dise˜na un programa que lea una serie de valores enteros y los vaya escribiendo en
un fichero hasta que el usuario introduzca el valor −1 (que no se escribir´a en el fichero). Tu
programa debe, a continuaci´on, determinar si la secuencia de n´umeros introducida en el fichero
es pal´ındroma.
· 337 Deseamos gestionar una colecci´on de c´omics. De cada c´omic anotamos los siguientes
datos:
Superh´eroe: una cadena de hasta 20 caracteres.
T´ıtulo: una cadena de hasta 200 caracteres.
N´umero: un entero.
A˜no: un entero.
Editorial: una cadena de hasta 30 caracteres.
Sinopsis: una cadena de hasta 1000 caracteres.
El programa permitir´a:
1. Dar de alta un c´omic.
2. Consultar la ficha completa de un c´omic dado el superh´eroe y el n´umero del episodio.
3. Ver un listado por superh´eroe que muestre el t´ıtulo de todas sus historias.
4. Ver un listado por a˜no que muestre el superh´erore y t´ıtulo de todas sus historias.
Dise˜na un programa que gestione la base de datos teniendo en cuenta que no queremos cargarla
en memoria cada vez que ejecutamos el programa, sino gestionarla directamente sobre disco.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.4. Errores
Algunas de las operaciones con ficheros pueden resultar fallidas (apertura de un fichero cuya
ruta no apunta a ning´un fichero existente, cierre de un fichero ya cerrado, etc.). Cuando as´ı
ocurre, la funci´on llamada devuelve un valor que indica que se cometi´o un error, pero ese valor
s´olo no aporta informaci´on que nos permita conocer el error cometido.
La informaci´on adicional est´a codificada en una variable especial: errno (declarada en
errno.h). Puedes comparar su valor con el de las constantes predefinidas en errno.h para
averiguar qu´e error concreto se ha cometido:
EACCESS: permiso denegado,
EEXIST: el fichero no existe,
EMFILE: demasiados ficheros abiertos,
. . .
Introducci´on a la Programaci´on con C 353
5.4 Errores
Truncamiento de ficheros
Las funciones est´andar de manejo de ficheros no permiten efectuar una operaci´on que puede
resultar necesaria en algunas aplicaciones: eliminar elementos de un fichero. Una forma de
conseguir este efecto consiste en generar un nuevo fichero en el que escribimos s´olo aquellos
elementos que no deseamos eliminar. Una vez generado el nuevo fichero, borramos el original
y renombramos el nuevo para que adopte el nombre del original. Costoso.
En Unix puedes recurrir a la funci´on truncate (disponible al incluir la cabecera
unistd.h). El perfil de truncate es ´este:
int truncate(char nombre[], int longitud);
La funci´on recibe el nombre de un fichero (que no debe estar abierto) y el n´umero de bytes
que deseamos conservar. Si la llamada tiene ´exito, la funci´on hace que en el fichero s´olo
permanezcan los longitud primeros bytes y devuelve el valor 0. En caso contrario, devuelve
el valor −1. Observa que s´olo puedes borrar los ´ultimos elementos de un fichero, y no
cualquiera de ellos. Por eso la acci´on de borrar parte de un fichero recibe el nombre de
truncamiento.
Como manejarte con tantas constantes (algunas con significados un tanto dif´ıcil de com-
prender hasta que curses asignaturas de sistemas operativos) resulta complicado, puedes usar
una funci´on especial:
void perror (char s[]);
Esta funci´on muestra por pantalla el valor de la cadena s, dos puntos y un mensaje de error que
detalla la causa del error cometido. La cadena s, que suministra el programador, suele indicar el
nombre de la funci´on en la que se detect´o el error, ayudando as´ı a la depuraci´on del programa.
354 Introducci´on a la Programaci´on con C
Ap´endice A
Tipos b´asicos
A.1. Enteros
A.1.1. Tipos
Esta tabla muestra el nombre de cada uno de los tipos de datos para valores enteros (algunos
tienen dos nombres v´alidos), su rango de representaci´on y el n´umero de bytes (grupos de 8 bits)
que ocupan.
Tipo Rango Bytes
char −128 . . . 127 1
short int (o short) −32768 . . . 32767 2
int −2147483648 . . . 2147483647 4
long int (o long) −2147483648 . . . 2147483647 4
long long int (o long long) −9223372036854775808 . . . 9223372036854775807 8
(Como ves, los tipos short int, long int y long long int pueden abreviarse, respectivamente,
como short, long, y long long.)
Un par de curiosidades sobre la tabla de tipos enteros:
Los tipos int y long int ocupan lo mismo (4 bytes) y tienen el mismo rango. Esto es as´ı
para el compilador gcc sobre un PC. En una m´aquina distinta o con otro compilador,
podr´ıan ser diferentes: los int podr´ıan ocupar 4 bytes y los long int, 8, por ejemplo. En
sistemas m´as antiguos un int ocupaba 2 bytes y un long int, 4.
El nombre del tipo char es abreviatura de ((car´acter)) (((character)), en ingl´es) y, sin em-
bargo, hace referencia a los enteros de 8 bits, es decir, 1 byte. Los valores de tipo char
son ambivalentes: son tanto n´umeros enteros como caracteres.
Es posible trabajar con enteros sin signo en C, es decir, n´umeros enteros positivos. La ventaja
de trabajar con ellos es que se puede aprovechar el bit de signo para aumentar el rango positivo
y duplicarlo. Los tipos enteros sin signo tienen el mismo nombre que sus correspondientes tipos
con signo, pero precedidos por la palabra unsigned, que act´ua como un adjetivo:
Tipo Rango Bytes
unsigned char 0. . . 255 1
unsigned short int (o unsigned short) 0. . . 65535 2
unsigned int (o unsigned) 0. . . 4294967295 4
unsigned long int (o unsigned long) 0. . . 4294967295 4
unsigned long long int (o unsigned long long) 0. . . 18446744073709551615 8
Del mismo modo que podemos ((marcar)) un tipo entero como ((sin signo)) con el adjetivo
unsigned, podemos hacer expl´ıcito que tiene signo con el adjetivo signed. O sea, el tipo
int puede escribirse tambi´en como signed int: son exactamente el mismo tipo, s´olo que en el
segundo caso se pone ´enfasis en que tiene signo, haciendo posible una mejora en la legibilidad
de un programa donde este rasgo sea importante.
Introducci´on a la Programaci´on con C 355
A.2 Flotantes
A.1.2. Literales
Puedes escribir n´umeros enteros en notaci´on octal (base 8) o hexadecimal (base 16). Un n´umero
en notaci´on hexadecimal empieza por 0x. Por ejemplo, 0xff es 255 y 0x0 es 0. Un n´umero en
notaci´on octal debe empezar por un 0 y no ir seguido de una x. Por ejemplo, 077 es 63 y 010
es 8.1
Puedes precisar que un n´umero entero es largo a˜nadi´endole el sufijo L (por ((Long))). Por
ejemplo, 2L es el valor 2 codificado con 32 bits. El sufijo LL (por ((long long))) indica que
el n´umero es un long long int. El literal 2LL, por ejemplo, representa al n´umero entero 2
codificado con 64 bits (lo que ocupa un long long int). El sufijo U (combinado opcionalmente
con L o LL) precisa que un n´umero no tiene signo (la U por ((unsigned))).
Normalmente no necesitar´as usar esos sufijos, pues C hace conversiones autom´aticas de tipo
cuando conviene. S´ı te har´a falta si quieres denotar un n´umero mayor que 2147483647 (o menor
que −2147483648), pues en tal caso el n´umero no puede representarse como un simple int. Por
ejemplo, la forma correcta de referirse a 3000000000 es con el literal 3000000000LL.
C resulta abrumador por la gran cantidad de posibilidades que ofrece. Son muchas formas
diferentes de representar enteros, ¿verdad? No te preocupes, s´olo en aplicaciones muy concretas
necesitar´as utilizar la notaci´on octal o hexadecimal o tendr´as que a˜nadir el sufijo a un literal
para indicar su tipo.
A.1.3. Marcas de formato
Hay una marca de formato para la impresi´on o lectura de valores de cada tipo de entero:
Tipo Marca Tipo Marca
char (n´umero) %hhd unsigned char %hhu
short %hd unsigned short %hu
int %d unsigned %u
long %ld unsigned long %lu
long long %lld unsigned long long %llu
Puedes mostrar los valores num´ericos en base octal o hexadecimal sustituyendo la d (o la u) por
una o o una x, respectivamente. Por ejemplo, %lx es una marca que muestra un entero largo
en hexadecimal y %ho muestra un short en octal.
Son muchas, ¿verdad? La que usar´as m´as frecuentemente es %d. De todos modos, por si
necesitas utilizar otras, he aqu´ı algunas reglas mnemot´ecnicas:
d significa ((decimal)) y alude a la base en que se representa la informaci´on: base 10. Por
otra parte, x y o representan a ((hexadecimal)) y ((octal)) y aluden a las bases 16 y 8.
u significa ((unsigned)), es decir, ((sin signo)).
h significa ((mitad)) (por ((half))), as´ı que %hd es ((la mitad)) de un entero, o sea, un short,
y %hhd es ((la mitad de la mitad)) de un entero, o sea, un char.
l significa ((largo)) (por ((long))), as´ı que %ld es un entero largo (un long) y %lld es un
entero extra-largo (un long long).
A.2. Flotantes
A.2.1. Tipos
Tambi´en en el caso de los flotantes tenemos d´onde elegir: hay tres tipos diferentes. En esta tabla
te mostramos el nombre, m´aximo valor absoluto y n´umero de bits de cada uno de ellos:
Tipo M´aximo valor absoluto Bytes
float 3.40282347·1038
4
double 1.7976931348623157·10308
8
long double 1.189731495357231765021263853031·104932
12
1Lo cierto es que tambi´en puede usar notaci´on octal o hexadecimal en Python, aunque en su momento no lo
contamos.
356 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia A Tipos b´asicos
Recuerda que los n´umeros expresados en coma flotante presentan mayor resoluci´on en la
cercan´ıas del 0, y que ´esta es tanto menor cuanto mayor es, en valor absoluto, el n´umero
representado. El n´umero no nulo m´as pr´oximo a cero que puede representarse con cada uno de
los tipos se muestra en esta tabla:
Tipo M´ınimo valor absoluto no nulo
float 1.17549435·10−38
double 2.2250738585072014·10−308
long double 3.3621031431120935062626778173218·10−4932
A.2.2. Literales
Ya conoces las reglas para formar literales para valores de tipo float. Puedes a˜nadir el sufijo F
para precisar que el literal corresponde a un double y el sufijo L para indicar que se trata de
un long double. Por ejemplo, el literal 3.2F es el valor 3.2 codificado como double. Al igual
que con los enteros, normalmente no necesitar´as precisar el tipo del literal con el sufijo L, a
menos que su valor exceda del rango propio de los float.
A.2.3. Marcas de formato
Veamos ahora las principales marcas de formato para la impresi´on de datos de tipos flo-
tantes:
Tipo Notaci´on convencional Notaci´on cient´ıfica
float %f %e
double %f %e
long double %Lf %Le
Observa que tanto float como double usan la misma marca de formato para impresi´on (o
sea, con la funci´on printf y similares).
No pretendemos detallar todas las marcas de formato para flotantes. Tenemos, adem´as,
otras como %E, %F, %g, %G, %LE, %LF, %Lg y %LG. Cada marca introduce ciertos matices que,
en seg´un qu´e aplicaciones, pueden venir muy bien. Necesitar´as un buen manual de referencia
a mano para controlar estos y otros muchos aspectos (no tiene sentido memorizarlos) cuando
ejerzas de programador en C durante tu vida profesional.2
Las marcas de formato para la lectura de datos de tipos flotantes presentan alguna
diferencia:
Tipo Notaci´on convencional
float %f
double %lf
long double %Lf
Observa que la marca de impresi´on de un double es %f, pero la de lectura es %lf. Es una
incoherencia de C que puede darte alg´un que otro problema.
A.3. Caracteres
El tipo char, que ya hemos presentado al estudiar los tipos enteros, es, a la vez el tipo con el
que solemos representar caracteres y con el que formamos las cadenas.
A.3.1. Literales
Los literales de car´acter encierran entre comillas simples al car´acter en cuesti´on o lo codifican
como un n´umero entero. Es posible utilizar secuencias de escape para indicar el car´acter que se
encierra entre comillas.
2En Unix puedes obtener ayuda acerca de las funciones est´andar con el manual en l´ınea. Ejecuta man 3
printf, por ejemplo, y obtendr´as una p´agina de manual sobre la funci´on printf , incluyendo informaci´on sobre
todas sus marcas de formato y modificadores.
Introducci´on a la Programaci´on con C 357
A.4 Otros tipos b´asicos
A.3.2. Marcas de formato
Los valores de tipo char pueden mostrarse en pantalla (o escribirse en ficheros de texto) usando
la marca %c o %hhd. La primera marca muestra el car´acter como eso mismo, como car´acter; la
segunda muestra su valor decimal (el c´odigo ASCII del car´acter).
A.4. Otros tipos b´asicos
C99 define tres nuevos tipos b´asicos: el tipo l´ogico (o booleano), el tipo complejo y el tipo
imaginario.
A.4.1. El tipo booleano
Las variables de tipo _Bool pueden almacenar los valores 0 (((falso))) o 1 (((cierto))). Si se incluye
la cabecera <stdbool.h> es posible usar el identificador de tipo bool y las constantes true y
false para referirse al tipo _Bool y a los valores 1 y 0, respectivamente.
A.4.2. Los tipos complejo e imaginario
C99 ofrece soporte para la aritm´etica compleja a trav´es de los tipos _Complex e _Imaginary.
A.5. Una reflexi´on acerca de la diversidad de tipos escalares
¿Por qu´e ofrece C tan gran variedad de tipos de datos para enteros y flotantes? Porque C
procura facilitar el dise˜no de programas eficientes proporcionando al programador un juego de
tipos que le permita adoptar el compromiso adecuado entre ocupaci´on de memoria y rango
disponible. ¿Por qu´e iba un programador a querer gastar 4 bytes en una variable que s´olo
almacenar´a valores entre 0 y 255? Naturalmente, ofrecer m´as control no es gratis: a cambio
hemos de tomar muchas m´as decisiones. Ahorrar 3 bytes en una variable puede no justificar el
quebradero de cabeza, pero piensa en el ahorro que se puede producir en un vector que contiene
miles o cientos de miles de elementos que pueden representarse cada uno con un char en lugar
de con un int.
Por otra parte, la arquitectura de tu ordenador est´a optimizada para realizar c´alculos con
valores de ciertos tama˜nos. Por ejemplo, las operaciones con enteros suelen ser m´as r´apidas
si trabajas con int (aunque ocupen m´as bytes que los char o short) y las operaciones con
flotantes m´as eficientes trabajan con double.
Seg´un si valoras m´as velocidad o consumo de memoria en una aplicaci´on, deber´as escoger
uno u otro tipo de datos para ciertas variables.
358 Introducci´on a la Programaci´on con C
Ap´endice B
La lectura de datos por teclado,
paso a paso
B.1. La lectura de valores escalares con scanf
La funci´on scanf (y fscanf ) se comporta de un modo un tanto especial y puede desconcertarte
en ocasiones. Veamos qu´e hace exactamente scanf :
Empieza salt´andose los blancos que encuentra (espacios en blanco, tabuladores y saltos
de l´ınea).
A continuaci´on, ((consume)) los caracteres no blancos mientra ((le sirvan)) para leer un valor
del tipo que se indica con la marca de formato (por ejemplo, d´ıgitos si la marca es %d).
La lectura se detiene cuando el siguiente car´acter a leer ((no sirve)) (por ejemplo, una
letra si estamos leyendo un entero). Dicho car´acter no es ((consumido)). Los caracteres
((consumidos)) hasta este punto se interpretan como la representaci´on de un valor del tipo
que se indica con la correspondiente marca de formato, as´ı que se crea dicho valor y se
escribe en la zona de memoria que empieza en la direcci´on que se indique.
Un ejemplo ayudar´a a entender el comportamiento de scanf :
lee tres.c lee tres.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, c;
6 float b;
7
8 printf ("Entero a: "); scanf ("%d", &a);
9 printf ("Flotante b: "); scanf ("%f", &b);
10 printf ("Entero c: "); scanf ("%d", &c);
11 printf ("El entero a es %d, el flotante b es %f y el entero c es %d
12 ", a, b, c);
13
14 return 0;
15 }
Ejecutemos el programa e introduzcamos los valores 20, 3.0 y 4 pulsando el retorno de carro
tras cada uno de ellos.
Entero a: 20
Flotante b: 3.0
Entero c: 4
El entero a es 20, el flotante b es 3.000000 y el entero c es 4
Perfecto. Para ver qu´e ha ocurrido paso a paso vamos a representar el texto que escribe
el usuario durante la ejecuci´on como una secuencia de teclas. En este gr´afico se muestra qu´e
Introducci´on a la Programaci´on con C 359
B.1 La lectura de valores escalares con scanf
ocurre durante la ejecuci´on del primer scanf (l´ınea 8), momento en el que las tres variables
est´an sin inicializar y el usuario acaba de pulsar las teclas 2, 0 y retorno de carro:
2 0 n a
b
c
El car´acter a la derecha de la flecha es el siguiente car´acter que va a ser consumido.
La ejecuci´on del primer scanf consume los caracteres ’2’ y ’0’, pues ambos son v´alidos
para formar un entero. La funci´on detecta el blanco (salto de l´ınea) que sigue al car´acter ’0’ y
se detiene. Interpreta entonces los caracteres que ha le´ıdo como el valor entero 20 y lo almacena
en la direcci´on de memoria que se la suministrado (&a):
2 0 n
&a
20a
b
c
En la figura hemos representado los caracteres consumidos en color gris. F´ıjate en que el salto
de l´ınea a´un no ha sido consumido.
La ejecuci´on del segundo scanf , el que lee el contenido de b, empieza descartando los blancos
iniciales, es decir, el salto de l´ınea:
2 0 n 20a
b
c
Como no hay m´as caracteres que procesar, scanf queda a la espera de que el usuario teclee algo
con lo que pueda formar un flotante y pulse retorno de carro. Cuando el usaurio teclea el 3.0
seguido del salto de l´ınea, pasamos a esta nueva situaci´on:
2 0 n 3 . 0 n 20a
b
c
Ahora, scanf reanuda su ejecuci´on y consume el ’3’, el ’.’ y el ’0’. Como detecta que lo que
sigue no es v´alido para formar un flotante, se detiene, interpreta los caracteres le´ıdos como el
valor flotante 3.0 y lo almacena en la direcci´on de b:
2 0 n 3 . 0 n 20a
&b
3.0b
c
Finalmente, el tercer scanf entra en ejecuci´on y empieza por saltarse el salto de l´ınea.
2 0 n 3 . 0 n 20a
3.0b
c
Acto seguido se detiene, pues no es necesario que el usuario introduzca nuevo texto que procesar.
Entonces el usuario escribe el 4 y pulsa retorno:
360 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia B La lectura de datos por teclado, paso a paso
2 0 n 3 . 0 n 4 n 20a
3.0b
c
Ahora scanf prosigue consumiendo el 4 y deteni´endose nuevamente ante el salto de l´ınea. El
car´acter le´ıdo se interpreta entonces como el entero 4 y se almacena en la direcci´on de memoria
de c:
2 0 n 3 . 0 n 4 n 20a
3.0b
&c
4c
Como puedes apreciar, el ´ultimo salto de l´ınea no llega a ser consumido, pero eso importa poco,
pues el programa finaliza correctamente su ejecuci´on.
Vamos a estudiar ahora el porqu´e de un efecto curioso. Imagina que, cuando el programa
pide al usuario el primer valor entero, ´este introduce tanto dicho valor como los dos siguientes,
separando los tres valores con espacios en blanco. He aqu´ı el resultado en pantalla:
Entero a: 20 3.0 4
Flotante b: Entero c: El entero a es 20, el flotante b es 3.000000 y el entero c es 4
El programa ha le´ıdo correctamente los tres valores, sin esperar a que el usuario introduzca
tres l´ıneas con datos: cuando ten´ıa que detenerse para leer el valor de b, no lo ha hecho, pues
((sab´ıa)) que ese valor era 3.0; y tampoco se ha detenido al leer el valor de c, ya que de alg´un
modo ((sab´ıa)) que era 4. Veamos paso a paso lo que ha sucedido, pues la explicaci´on es bien
sencilla.
Durante la ejecuci´on del primer scanf , el usuario ha escrito el siguiente texto:
2 0 3 . 0 4 n a
b
c
Como su objetivo es leer un entero, ha empezado a consumir caracteres. El ’2’ y el ’0’ le
son ´ultiles, as´ı que los ha consumido. Entonces se ha detenido frente al espacio en blanco. Los
caracteres le´ıdos se interpretan como el valor entero 20 y se almacenan en a:
2 0 3 . 0 4 n
&a
20a
b
c
La ejecuci´on del siguiente scanf no ha detenido la ejecuci´on del programa, pues a´un hab´ıa
caracteres pendientes de procesar en la entrada. Como siempre, scanf se ha saltado el primer
blanco y ha ido encontrando caracteres v´alidos para ir formando un valor del tipo que se le
indica (en este caso, un flotante). La funci´on scanf ha dejado de consumir caracteres al encontrar
un nuevo blanco, se ha detenido y ha almacenado en b el valor flotante 3.0. He aqu´ı el nuevo
estado:
2 0 3 . 0 4 n 20a
&b
3.0b
c
Introducci´on a la Programaci´on con C 361
B.2 La lectura de cadenas con scanf
Finalmente, el tercer scanf tampoco ha esperado nueva entrada de teclado: se ha saltado direc-
tamente el siguiente blanco, ha encontrado el car´acter ’4’ y se ha detenido porque el car´acter
n que le sigue es un blanco. El valor le´ıdo (el entero 4) se almacena en c:
2 0 3 . 0 4 n 20a
3.0b
&c
4c
Tras almacenar en c el entero 4, el estado es ´este:
2 0 3 . 0 4 n 20a
3.0b
4c
Cuando observes un comportamiento inesperado de scanf , haz un an´alisis de lo sucedido
como el que te hemos presentado aqu´ı y ver´as que todo tiene explicaci´on.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 338 ¿Qu´e pasa si el usuario escribe la siguiente secuencia de caracteres como datos de
entrada en la ejecuci´on del programa?
2 0 3 . 1 2 3 4 5 5 n
· 339 ¿Qu´e pasa si el usuario escribe la siguiente secuencia de caracteres como datos de
entrada en la ejecuci´on del programa?
2 0 3 . 1 2 3 n 4 5 5 n
· 340 ¿Qu´e pasa si el usuario escribe la siguiente secuencia de caracteres como datos de
entrada en la ejecuci´on del programa?
2 0 2 4 5 x n
· 341 ¿Qu´e pasa si el usuario escribe la siguiente secuencia de caracteres como datos de
entrada en la ejecuci´on del programa?
6 x 2 n
(Prueba este ejercicio con el ordenador.)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
B.2. La lectura de cadenas con scanf
Vamos a estudiar ahora el comportamiento paso a paso de scanf cuando leemos una cadena:
Se descartan los blancos iniciales (espacios en blanco, tabuladores o saltos de l´ınea).
Se leen los caracteres ((v´alidos)) hasta el primer blanco y se almacenan en posiciones de
memoria consecutivas a partir de la que se suministra como argumento. Se entiende por
car´acter v´alido cualquier car´acter no blanco (ni tabulador, ni espacio en blanco, ni salto
de l´ınea. . . ).
Se a˜nade al final un terminador de cadena.
Un ejemplo ayudar´a a entender qu´e ocurre ante ciertas entradas:
362 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia B La lectura de datos por teclado, paso a paso
lee cadena.c lee cadena.c
1 #include <stdio.h>
2
3 #define TALLA 10
4
5 int main(void)
6 {
7 char a[TALLA+1], b[TALLA+1];
8
9 printf ("Cadena 1: "); scanf ("%s", a);
10 printf ("Cadena 2: "); scanf ("%s", b);
11 printf ("La cadena 1 es %s y la cadena 2 es %sn", a, b);
12
13 return 0;
14 }
Si ejecutas el programa y escribes una primera cadena sin blancos, pulsas el retorno de carro,
escribes otra cadena sin blancos y vuelves a pulsar el retorno, la lectura se efect´ua como cabe
esperar:
Cadena 1: uno
Cadena 2: dos
La cadena 1 es uno y la cadena 2 es dos
Estudiemos paso a paso lo ocurrido. Ante el primer scanf , el usuario ha escrito lo siguiente:
u n o n
La funci´on ha empezado a consumir los caracteres con los que ir formando la cadena. Al llegar
al salto de l´ınea se ha detenido sin consumirlo. He aqu´ı el nuevo estado de cosas:
u n o n
a u
0
n
1
o
2
0
3 4 5 6 7 8 9
(F´ıjate en que scanf termina correctamente la cadena almacenada en a.) Acto seguido se ha
ejecutado el segundo scanf . La funci´on se salta entonces el blanco inicial, es decir, el salto de
l´ınea que a´un no hab´ıa sido consumido.
u n o n
a u
0
n
1
o
2
0
3 4 5 6 7 8 9
Como no hay m´as caracteres, scanf ha detenido la ejecuci´on a la espera de que el usuario teclee
algo. Entonces el usuario ha escrito la palabra dos y ha pulsado retorno de carro:
u n o n d o s n
a u
0
n
1
o
2
0
3 4 5 6 7 8 9
Entonces scanf ha procedido a consumir los tres primeros caracteres:
Introducci´on a la Programaci´on con C 363
B.2 La lectura de cadenas con scanf
u n o n d o s n
a u
0
n
1
o
2
0
3 4 5 6 7 8 9
b d
0
o
1
s
2
0
3 4 5 6 7 8 9
F´ıjate en que scanf introduce autom´aticamente el terminador pertinente al final de la cadena
le´ıda. El segundo scanf nos conduce a esta nueva situaci´on:
u n o n d o s n
a u
0
n
1
o
2
0
3 4 5 6 7 8 9
b d
0
o
1
s
2
0
3 4 5 6 7 8 9
Compliquemos un poco la situaci´on. ¿Qu´e ocurre si, al introducir las cadenas, metemos
espacios en blanco delante y detr´as de las palabras?
Cadena 1: uno
Cadena 2: dos
La cadena 1 es uno y la cadena 2 es dos
Recuerda que scanf se salta siempre los blancos que encuentra al principio y que se detiene
en el primer espacio que encuentra tras empezar a consumir caracteres v´alidos. Ve´amoslo paso
a paso. Empezamos con este estado de la entrada:
u n o n
El primer scanf empieza salt´andose los blancos inciales:
u n o n
A continuaci´on consume los caracteres ’u’, ’n’ y ’o’ y se detiene al detectar el blanco que
sigue:
u n o n
a u
0
n
1
o
2
0
3 4 5 6 7 8 9
Cuando se ejecuta, el segundo scanf empieza salt´andose los blancos iniciales, que son todos los
que hay hasta el salto de l´ınea (inclu´ıdo ´este):
u n o n
a u
0
n
1
o
2
0
3 4 5 6 7 8 9
De nuevo, como no hay m´as que leer, la ejecuci´on se detiene. El usuario teclea entonces nuevos
caracteres:
364 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia B La lectura de datos por teclado, paso a paso
u n o n d o s n
a u
0
n
1
o
2
0
3 4 5 6 7 8 9
A continuaci´on, sigue salt´andose los blancos:
u n o n d o s n
a u
0
n
1
o
2
0
3 4 5 6 7 8 9
Pasa entonces a consumir caracteres no blancos y se detiene ante el primer blanco:
u n o n d o s n
a u
0
n
1
o
2
0
3 4 5 6 7 8 9
b d
0
o
1
s
2
0
3 4 5 6 7 8 9
Ya est´a.
Imagina ahora que nuestro usuario quiere introducir en a la cadena "uno dos" y en b la
cadena "tres". Aqu´ı tienes lo que ocurre al ejecutar el programa
Cadena 1: uno dos
Cadena 2: La cadena 1 es uno y la cadena 2 es dos
El programa ha finalizado sin darle tiempo al usuario a introducir la cadena "tres". Es
m´as, la primera cadena vale "uno" y la segunda "dos", con lo que ni siquiera se ha conseguido
el primer objetivo: leer la cadena "uno dos" y depositarla tal cual en a. Analicemos paso a
paso lo sucedido. La entrada que el usuario teclea ante el primer scanf es ´esta:
u n o d o s n
La funci´on lee en a los caracteres ’u’, ’n’ y ’o’ y se detiene al detectar un blanco. El nuevo
estado se puede representar as´ı:
u n o d o s n
a u
0
n
1
o
2
0
3 4 5 6 7 8 9
El segundo scanf entra en juego entonces y ((aprovecha)) lo que a´un no ha sido procesado, as´ı
que empieza por descartar el blanco inicial y, a continuaci´on, consume los caracteres ’d’, ’o’,
’s’:
u n o d o s n
a u
0
n
1
o
2
0
3 4 5 6 7 8 9
b d
0
o
1
s
2
0
3 4 5 6 7 8 9
Introducci´on a la Programaci´on con C 365
B.3 Un problema serio: la lectura alterna de cadenas con gets y de escalares con scanf
¿Ves? La consecuencia de este comportamiento es que con scanf s´olo podemos leer palabras
individuales. Para leer una l´ınea completa en una cadena, hemos de utilizar una funci´on distinta:
gets (por ((get string)), que en ingl´es significa ((obt´en cadena))), disponible incluyendo stdio.h
en nuestro programa.
B.3. Un problema serio: la lectura alterna de cadenas con gets
y de escalares con scanf
Vamos a estudiar un caso concreto y analizaremos las causas del extra˜no comportamiento
observado.
lee alterno mal.c lee alterno mal.c
1 #include <stdio.h>
2
3 #define TALLA 80
4
5 int main(void)
6 {
7 char a[TALLA+1], b[TALLA+1];
8 int i;
9
10 printf ("Cadena a: "); gets(a);
11 printf ("Entero i: "); scanf ("%d", &i);
12 printf ("Cadena b: "); gets(b);
13 printf ("La cadena a es %s, el entero i es %d y la cadena b es %sn", a, i, b);
14
15 return 0;
16 }
Observa que leemos cadenas con gets y un entero con scanf . Vamos a ejecutar el programa
introduciendo la palabra uno en la primera cadena, el valor 2 en el entero y la palabra dos en
la segunda cadena.
Cadena a: uno
Entero i: 2
Cadena b: La cadena a es uno, el entero i es 2 y la cadena b es
¿Qu´e ha pasado? No hemos podido introducir la segunda cadena: ¡tan pronto hemos escrito
el retorno de carro que sigue al 2, el programa ha finalizado! Estudiemos paso a paso lo ocurrido.
El texto introducido ante el primer scanf es:
u n o n
El primer gets nos deja en esta situaci´on:
u n o n
a u
0
n
1
o
2
0
3 4 5 6 7 8 9
A continuaci´on se ejecuta el scanf con el que se lee el valor de i. El usuario teclea lo siguiente:
u n o n 2 n
a u
0
n
1
o
2
0
3 4 5 6 7 8 9
2i
366 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia B La lectura de datos por teclado, paso a paso
La funci´on lee el 2 y encuentra un salto de l´ınea. El estado en el que queda el programa es ´este:
u n o n 2 n
a u
0
n
1
o
2
0
3 4 5 6 7 8 9
2i
F´ıjate bien en qu´e ha ocurrido: nos hemos quedado a las puertas de procesar el salto de l´ınea.
Cuando el programa pasa a ejecutar el siguiente gets, ¡lee una cadena vac´ıa! ¿Por qu´e? Porque
gets lee caracteres hasta el primer salto de l´ınea, y el primer car´acter con que nos encontramos
ya es un salto de l´ınea. Pasamos, pues, a este nuevo estado:
u n o n 2 n
a u
0
n
1
o
2
0
3 4 5 6 7 8 9
2i
b 0
0 1 2 3 4 5 6 7 8 9
¿C´omo podemos evitar este problema? Una soluci´on posible consiste en consumir la cadena
vac´ıa con un gets extra y una variable auxiliar. F´ıjate en este programa:
lee alterno bien.c lee alterno bien.c
1 #include <stdio.h>
2
3 #define TALLA 80
4
5 int main(void)
6 {
7 char a[TALLA+1], b[TALLA+1];
8 int i;
9 char findelinea[TALLA+1]; // Cadena auxiliar. Su contenido no nos importa.
10
11 printf ("Cadena a: "); gets(a);
12 printf ("Entero i: "); scanf ("%d", &i); gets(findelinea);
13 printf ("Cadena b: "); gets(b);
14 printf ("La cadena a es %s, el entero i es %d y la cadena b es %sn", a, i, b);
15
16 return 0;
17 }
Hemos introducido una variable extra, findelinea, cuyo ´unico objetivo es consumir lo que scanf
no ha consumido. Gracias a ella, ´este es el estado en que nos encontramos justo antes de empezar
la lectura de b:
u n o n 2 n
a u
0
n
1
o
2
0
3 4 5 6 7 8 9
2i
findelinea 0
0 1 2 3 4 5 6 7 8 9
Introducci´on a la Programaci´on con C 367
B.3 Un problema serio: la lectura alterna de cadenas con gets y de escalares con scanf
El usuario escribe entonces el texto que desea almacenar en b:
u n o n 2 n d o s n
a u
0
n
1
o
2
0
3 4 5 6 7 8 9
2i
findelinea 0
0 1 2 3 4 5 6 7 8 9
Ahora la lectura de b tiene ´exito. Tras ejecutar gets, ´este es el estado resultante:
u n o n 2 n d o s n
a u
0
n
1
o
2
0
3 4 5 6 7 8 9
2i
findelinea 0
0 1 2 3 4 5 6 7 8 9
b d
0
o
1
s
2
0
3 4 5 6 7 8 9
¡Perfecto! Ya te dijimos que aprender C iba a suponer enfrentarse a algunas dificultades de
car´acter t´ecnico. La ´unica forma de superarlas es conocer bien qu´e ocurre en las entra˜nas del
programa.
Pese a que esta soluci´on funciona, facilita la comisi´on de errores. Hemos de recordar consumir
el fin de l´ınea s´olo en ciertos contexto. Esta otra soluci´on es m´as sistem´atica: leer siempre l´ınea
a l´ınea con gets y, cuando hay de leerse un dato entero, flotante, etc., hacerlo con sscanf sobre
la cadena le´ıda:
lee alterno bien.c lee alterno bien.c
1 #include <stdio.h>
2
3 #define TALLA 80
4
5 int main(void)
6 {
7 char a[TALLA+1], b[TALLA+1];
8 int i;
9 char linea[TALLA+1]; // Cadena auxiliar. Su contenido no nos importa.
10
11 printf ("Cadena a: "); gets(a);
12 printf ("Entero i: "); gets(linea); sscanf (linea , "%d", &i);
13 printf ("Cadena b: "); gets(b);
14 printf ("La cadena a es %s, el entero i es %d y la cadena b es %sn", a, i, b);
15
16 return 0;
17 }
368 Introducci´on a la Programaci´on con C
CC 2003, 2008 Andr´es Marzal e Isabel Gracia B La lectura de datos por teclado, paso a paso
((¡Ah, ya s´e!, ¡es un libro del Espejo, naturalmente! Si lo pongo delante de un espejo,
las palabras se ver´an otra vez al derecho.))
Y ´este es el poema que ley´o Alicia11
:
JERIG ´ONDOR
Cocillaba el d´ıa y las tovas agilimosas
giroscopaban y barrenaban en el larde.
Todos debirables estaban los burgovos,
y silbramaban las alecas rastas.
11. [...] Carroll pasa a continuaci´on a interpretar las palabras de la manera siguiente:
Bryllig [“cocillaba”] (der. del verbo “Bryl” o “Broil”); “hora de cocinar la comida;
es decir, cerca de la hora de comer”.
Slythy [“agilimosas”] (voz compuesta por “Slimy” y “Lithe”. “Suave y activo”.
Tova. Especie de tej´on. Ten´ıa suave pelo blanco, largas patas traseras y cuernos cortos
como de ciervo, se alimentaba principalmente de queso.
Gyre [“giroscopar”], verbo (derivado de Gyaour o Giaour, “perro”). “Ara˜nar como
un perro”.
Gymble [“barrenar”], (de donde viene Gimblet [“barrena”]) “hacer agujeros en algo”.
Wave [“larde”] (derivado del verbo “to swab” [“fregar”] o “soak” [“empapar”]). “Ladera
de una colina” (del hecho de empaparse por acci´on de la lluvia).
Mimsy (de donde viene Mimserable y Miserable): “infeliz”.
Borogove [“burgovo”], especie extinguida de loro. Carec´ıa de alas, ten´ıa el pico hacia
arriba, y anidaba bajo los relojes de sol: se alimentaba de ternera.
Mome [“aleca”] (de donde viene Solemome y Solemne). Grave.
Rath [“rasta”]. Especie de tortuga de tierra. Cabeza erecta, boca de tibur´on, patas
anteriores torcidas, de manera que el animal caminaba sobre sus rodillas; cuerpo liso de
color verde; se alimentaba de golondrinas y ostras.
Outgrabe [“silbramar”]. Pret´erito del verbo Outgribe (emparentado con el antiguo
to Grike o Shrike, del que proceden “Shreak” [“chillar”] y “Creak” [“chirriar”]:
“chillaban”.
Por tanto, el pasaje dice literalmente: “Era por la tarde, y los tejones, suaves y activos, hurgaban
y hac´ıan agujeros en las laderas; los loros eran muy desdichados, y las graves tortugas profer´ıan
chillidos.”
Alicia anotada (Edici´on de Martin Gardner), Lewis Carroll.
Introducci´on a la Programaci´on con C 369

Más contenido relacionado

PDF
Introduccion al lenguaje c
PDF
Introduccion a los Sistemas Digitales
PDF
Manual referencia cxx
PDF
Guia vim
PDF
El lenguaje de programación c++
PDF
Manual de programacion lenguaje en C
PDF
Pensar en cpp
PDF
Curso de html y phpnuke
Introduccion al lenguaje c
Introduccion a los Sistemas Digitales
Manual referencia cxx
Guia vim
El lenguaje de programación c++
Manual de programacion lenguaje en C
Pensar en cpp
Curso de html y phpnuke

La actualidad más candente (12)

PDF
Curso logica digital unam
PDF
Libro javacontapa
PDF
Seminario del Operador
PDF
Fs 1035 mfp-1135mfpspog.2011.12
PDF
Desarrollo proyectos-informaticos-con-java
PDF
PDF
Manual excitador-vaca
PDF
Electronicadigital
PDF
Ecosys m2030dn ...535dn_og_es
PDF
Curso logica digital unam
Libro javacontapa
Seminario del Operador
Fs 1035 mfp-1135mfpspog.2011.12
Desarrollo proyectos-informaticos-con-java
Manual excitador-vaca
Electronicadigital
Ecosys m2030dn ...535dn_og_es
Publicidad

Destacado (14)

PPT
Introducción a la Programación Neurolingüística
PPT
2 El Pecado Original
PDF
La globalización, la competencia y el futuro. ¿Por qué hay que ser creativo?
PDF
C++11
PDF
Introduccion del Lenguaje C
PPTX
PPTX
Programación neurolinguistica
PDF
Metodos numericos con matlab
PDF
Matlab: una_introduccion_con_ejemplos_practicos
PDF
Fundamentos de la programacion (Luis Joyanes) 3era Edicion
PDF
Fundamentos de programacion
PDF
"Introducción a la PNL" By: Joseph O'connor
PPTX
PROGRAMACION NEUROLINGUISTICA
PDF
PNL y Estrategias
Introducción a la Programación Neurolingüística
2 El Pecado Original
La globalización, la competencia y el futuro. ¿Por qué hay que ser creativo?
C++11
Introduccion del Lenguaje C
Programación neurolinguistica
Metodos numericos con matlab
Matlab: una_introduccion_con_ejemplos_practicos
Fundamentos de la programacion (Luis Joyanes) 3era Edicion
Fundamentos de programacion
"Introducción a la PNL" By: Joseph O'connor
PROGRAMACION NEUROLINGUISTICA
PNL y Estrategias
Publicidad

Similar a Libro programación-en-c++ (20)

PDF
MANUAL DE LENGUAJE C
PDF
PDF
Introducción a la programación en c
PDF
Introducción a la programación en C
PDF
ApuntesC++.pdf
PDF
PDF
Fundamentos de Programación con Lenguaje de Programación C++
PDF
Programacion en Phyton desde ce..........................ro
PDF
pensar_en_cpp-vol1.pdf
PDF
Practicas estructuras de datos y algoritmos
PPTX
Introducción
PPTX
Introducción
PDF
Algoritmos programacion-python
PDF
Programación en c j. carlos lopez ardao
PDF
Algoritmos programacion-python
PDF
Algoritmos y programacion_i_-_con_lengua
PPTX
TUTORIAL DE LENGUAJE C
PPTX
TUTORIAL LENGUAJE C
PDF
Manual completo python
MANUAL DE LENGUAJE C
Introducción a la programación en c
Introducción a la programación en C
ApuntesC++.pdf
Fundamentos de Programación con Lenguaje de Programación C++
Programacion en Phyton desde ce..........................ro
pensar_en_cpp-vol1.pdf
Practicas estructuras de datos y algoritmos
Introducción
Introducción
Algoritmos programacion-python
Programación en c j. carlos lopez ardao
Algoritmos programacion-python
Algoritmos y programacion_i_-_con_lengua
TUTORIAL DE LENGUAJE C
TUTORIAL LENGUAJE C
Manual completo python

Último (20)

DOCX
PROYECTO DE APRENDIZAJE para la semana de fiestas patrias
PDF
Tomo 1 de biologia gratis ultra plusenmas
PDF
Didactica de la Investigacion Educativa SUE Ccesa007.pdf
PPTX
caso clínico iam clinica y semiología l3.pptx
PDF
1. Intrdoduccion y criterios de seleccion de Farm 2024.pdf
PDF
Unidad de Aprendizaje 5 de Educacion para el Trabajo EPT Ccesa007.pdf
PDF
5°-UNIDAD 5 - 2025.pdf aprendizaje 5tooo
PDF
Romper el Circulo de la Creatividad - Colleen Hoover Ccesa007.pdf
DOCX
V UNIDAD - PRIMER GRADO. del mes de agosto
PDF
COMUNICACION EFECTIVA PARA LA EDUCACION .pdf
PDF
Unidad de Aprendizaje 5 de Matematica 1ro Secundaria Ccesa007.pdf
PDF
Salvese Quien Pueda - Andres Oppenheimer Ccesa007.pdf
DOCX
III Ciclo _ Plan Anual 2025.docx PARA ESTUDIANTES DE PRIMARIA
DOCX
UNIDAD DE APRENDIZAJE 5 AGOSTO tradiciones
DOCX
2 GRADO UNIDAD 5 - 2025.docx para primaria
PDF
biología es un libro sobre casi todo el tema de biología
PDF
TRAUMA_Y_RECUPERACION consecuencias de la violencia JUDITH HERMAN
PDF
Guia de Tesis y Proyectos de Investigacion FS4 Ccesa007.pdf
PDF
Educación Artística y Desarrollo Humano - Howard Gardner Ccesa007.pdf
PDF
Cronograma de clases de Práctica Profesional 2 2025 UDE.pdf
PROYECTO DE APRENDIZAJE para la semana de fiestas patrias
Tomo 1 de biologia gratis ultra plusenmas
Didactica de la Investigacion Educativa SUE Ccesa007.pdf
caso clínico iam clinica y semiología l3.pptx
1. Intrdoduccion y criterios de seleccion de Farm 2024.pdf
Unidad de Aprendizaje 5 de Educacion para el Trabajo EPT Ccesa007.pdf
5°-UNIDAD 5 - 2025.pdf aprendizaje 5tooo
Romper el Circulo de la Creatividad - Colleen Hoover Ccesa007.pdf
V UNIDAD - PRIMER GRADO. del mes de agosto
COMUNICACION EFECTIVA PARA LA EDUCACION .pdf
Unidad de Aprendizaje 5 de Matematica 1ro Secundaria Ccesa007.pdf
Salvese Quien Pueda - Andres Oppenheimer Ccesa007.pdf
III Ciclo _ Plan Anual 2025.docx PARA ESTUDIANTES DE PRIMARIA
UNIDAD DE APRENDIZAJE 5 AGOSTO tradiciones
2 GRADO UNIDAD 5 - 2025.docx para primaria
biología es un libro sobre casi todo el tema de biología
TRAUMA_Y_RECUPERACION consecuencias de la violencia JUDITH HERMAN
Guia de Tesis y Proyectos de Investigacion FS4 Ccesa007.pdf
Educación Artística y Desarrollo Humano - Howard Gardner Ccesa007.pdf
Cronograma de clases de Práctica Profesional 2 2025 UDE.pdf

Libro programación-en-c++

  • 1. Introducci´on a la programaci´on con C Andr´es Marzal Isabel Gracia Departamento de Lenguajes y Sistemas Inform´aticos Universitat Jaume I
  • 2. Obra distribuida con licencia Creative Commons Esta obra se distribuye con licencia Creative Commons en su modalidad ((Reconocimiento-No Comercial-Sin obras derivadas 2.5 Espa˜na)). Usted es libre de copiar, distribuir y comunicar p´ublicamente la obra bajo las condiciones siguientes: Reconocimiento. Debe reconocer los cr´editos de la obra de la manera especificada por el autor o el licenciador (pero no de una manera que sugiera que tiene su apoyo o apoyan el uso que hace de su obra). No comercial. No puede utilizar esta obra para fines comerciales. Sin obras derivadas. No se puede alterar, transformar o generar una obra derivada a partir de esta obra. Este texto es un resumen de la licencia. El texto completo de la licencia se encuentra en http://creativecommons.org/licenses/by-nc-nd/2.5/es/legalcode.es. ii Introducci´on a la Programaci´on con C
  • 3. ´Indice general 1. Introducci´on a C 1 1.1. C es un lenguaje compilado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.2. Traduciendo de Python a C: una gu´ıa r´apida . . . . . . . . . . . . . . . . . . . . 5 1.3. Estructura t´ıpica de un programa C . . . . . . . . . . . . . . . . . . . . . . . . . 12 1.4. C es un lenguaje de formato libre . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 1.5. Hay dos tipos de comentario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 1.6. Valores literales en C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 1.6.1. Enteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 1.6.2. Flotantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 1.6.3. Cadenas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 1.7. C tiene un rico juego de tipos escalares . . . . . . . . . . . . . . . . . . . . . . . . 23 1.7.1. El tipo int . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 1.7.2. El tipo unsigned int . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 1.7.3. El tipo float . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 1.7.4. El tipo char . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 1.7.5. El tipo unsigned char . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 1.8. Se debe declarar el tipo de toda variable antes de usarla . . . . . . . . . . . . . . 24 1.8.1. Identificadores v´alidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 1.8.2. Sentencias de declaraci´on . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 1.8.3. Declaraci´on con inicializaci´on . . . . . . . . . . . . . . . . . . . . . . . . . 26 1.9. Salida por pantalla . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 1.9.1. Marcas de formato para la impresi´on de valores con printf . . . . . . . . . 27 1.10. Variables y direcciones de memoria . . . . . . . . . . . . . . . . . . . . . . . . . . 31 1.11. Entrada por teclado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 1.12. Expresiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 1.13. Conversi´on impl´ıcita y expl´ıcita de tipos . . . . . . . . . . . . . . . . . . . . . . . 41 1.14. Las directivas y el preprocesador . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 1.15. Constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 1.15.1. Definidas con la directiva define . . . . . . . . . . . . . . . . . . . . . . . 44 1.15.2. Definidas con el adjetivo const . . . . . . . . . . . . . . . . . . . . . . . . 44 1.15.3. Con tipos enumerados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 1.16. Las bibliotecas (m´odulos) se importan con #include . . . . . . . . . . . . . . . . 47 1.16.1. La biblioteca matem´atica . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 1.17. Estructuras de control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 1.17.1. Estructuras de control condicionales . . . . . . . . . . . . . . . . . . . . . 49 1.17.2. Estructuras de control iterativas . . . . . . . . . . . . . . . . . . . . . . . 54 1.17.3. Sentencias para alterar el flujo iterativo . . . . . . . . . . . . . . . . . . . 59 2. Estructuras de datos en C: vectores est´aticos y registros 63 2.1. Vectores est´aticos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 2.1.1. Declaraci´on de vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 2.1.2. Inicializaci´on de los vectores . . . . . . . . . . . . . . . . . . . . . . . . . . 64 2.1.3. Un programa de ejemplo: la criba de Erat´ostenes . . . . . . . . . . . . . . 65 2.1.4. Otro programa de ejemplo: estad´ısticas . . . . . . . . . . . . . . . . . . . 68 2.1.5. Otro programa de ejemplo: una calculadora para polinomios . . . . . . . . 77 2.1.6. Disposici´on de los vectores en memoria . . . . . . . . . . . . . . . . . . . . 83 2.1.7. Algunos problemas de C: accesos il´ıcitos a memoria . . . . . . . . . . . . 87 Introducci´on a la Programaci´on con C i
  • 4. ´INDICE GENERAL 2.1.8. Asignaci´on y copia de vectores . . . . . . . . . . . . . . . . . . . . . . . . 88 2.1.9. Comparaci´on de vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 2.2. Cadenas est´aticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 2.2.1. Declaraci´on de cadenas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 2.2.2. Representaci´on de las cadenas en memoria . . . . . . . . . . . . . . . . . . 91 2.2.3. Entrada/salida de cadenas . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 2.2.4. Asignaci´on y copia de cadenas . . . . . . . . . . . . . . . . . . . . . . . . 97 2.2.5. Longitud de una cadena . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101 2.2.6. Concatenaci´on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 2.2.7. Comparaci´on de cadenas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106 2.2.8. Funciones ´utiles para manejar caracteres . . . . . . . . . . . . . . . . . . . 107 2.2.9. Escritura en cadenas: sprintf . . . . . . . . . . . . . . . . . . . . . . . . . 108 2.2.10. Un programa de ejemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 2.3. Vectores multidimensionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 2.3.1. Sobre la disposici´on de los vectores multidimensionales en memoria . . . . 110 2.3.2. Un ejemplo: c´alculo matricial . . . . . . . . . . . . . . . . . . . . . . . . . 111 2.3.3. Vectores de cadenas, matrices de caracteres . . . . . . . . . . . . . . . . . 117 2.4. Registros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123 2.4.1. Un ejemplo: registros para almacenar vectores de talla variable (pero acotada) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127 2.4.2. Un ejemplo: rectas de regresi´on para una serie de puntos en el plano . . . 129 2.4.3. Otro ejemplo: gesti´on de una coleci´on de CDs . . . . . . . . . . . . . . . . 132 2.5. Definici´on de nuevos tipos de datos . . . . . . . . . . . . . . . . . . . . . . . . . . 136 3. Funciones 139 3.1. Definici´on de funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139 3.2. Variables locales y globales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143 3.2.1. Variables locales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143 3.2.2. Variables globales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145 3.3. Funciones sin par´ametros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146 3.4. Procedimientos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148 3.5. Paso de par´ametros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149 3.5.1. Par´ametros escalares: paso por valor . . . . . . . . . . . . . . . . . . . . . 149 3.5.2. Organizaci´on de la memoria: la pila de llamadas a funci´on . . . . . . . . . 149 3.5.3. Vectores de longitud variable . . . . . . . . . . . . . . . . . . . . . . . . . 155 3.5.4. Par´ametros vectoriales: paso por referencia . . . . . . . . . . . . . . . . . 155 3.5.5. Par´ametros escalares: paso por referencia mediante punteros . . . . . . . . 161 3.5.6. Paso de registros a funciones . . . . . . . . . . . . . . . . . . . . . . . . . 166 3.5.7. Paso de matrices y otros vectores multidimensionales . . . . . . . . . . . . 169 3.5.8. Tipos de retorno v´alidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173 3.5.9. Un ejercicio pr´actico: miniGalaxis . . . . . . . . . . . . . . . . . . . . . . 173 3.6. Recursi´on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190 3.6.1. Un m´etodo recursivo de ordenaci´on: mergesort . . . . . . . . . . . . . . . 191 3.6.2. Recursi´on indirecta y declaraci´on anticipada . . . . . . . . . . . . . . . . . 197 3.7. Macros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198 3.8. Otras cuestiones acerca de las funciones . . . . . . . . . . . . . . . . . . . . . . . 201 3.8.1. Funciones inline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201 3.8.2. Variables locales static . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202 3.8.3. Paso de funciones como par´ametros . . . . . . . . . . . . . . . . . . . . . 203 3.9. M´odulos, bibliotecas y unidades de compilaci´on . . . . . . . . . . . . . . . . . . . 205 3.9.1. Declaraci´on de prototipos en cabeceras . . . . . . . . . . . . . . . . . . . . 207 3.9.2. Declaraci´on de variables en cabeceras . . . . . . . . . . . . . . . . . . . . 209 3.9.3. Declaraci´on de registros en cabeceras . . . . . . . . . . . . . . . . . . . . . 210 ii Introducci´on a la Programaci´on con C
  • 5. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 0 ´INDICE GENERAL 4. Estructuras de datos: memoria din´amica 213 4.1. Vectores din´amicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213 4.1.1. malloc, free y NULL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214 4.1.2. Algunos ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217 4.1.3. Cadenas din´amicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229 4.2. Matrices din´amicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230 4.2.1. Gesti´on de memoria para matrices din´amicas . . . . . . . . . . . . . . . . 230 4.2.2. Definici´on de un tipo ((matriz din´amica)) y de funciones para su gesti´on . 234 4.3. M´as all´a de las matrices din´amicas . . . . . . . . . . . . . . . . . . . . . . . . . . 238 4.3.1. Vectores de vectores de tallas arbitrarias . . . . . . . . . . . . . . . . . . . 238 4.3.2. Arreglos n-dimensionales . . . . . . . . . . . . . . . . . . . . . . . . . . . 242 4.4. Redimensionamiento de la reserva de memoria . . . . . . . . . . . . . . . . . . . 243 4.4.1. Una aplicaci´on: una agenda telef´onica . . . . . . . . . . . . . . . . . . . . 251 4.5. Introducci´on a la gesti´on de registros enlazados . . . . . . . . . . . . . . . . . . . 256 4.5.1. Definici´on y creaci´on de la lista . . . . . . . . . . . . . . . . . . . . . . . . 258 4.5.2. Adici´on de nodos (por cabeza) . . . . . . . . . . . . . . . . . . . . . . . . 259 4.5.3. Adici´on de un nodo (por cola) . . . . . . . . . . . . . . . . . . . . . . . . 261 4.5.4. Borrado de la cabeza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265 4.6. Listas con enlace simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267 4.6.1. Creaci´on de lista vac´ıa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268 4.6.2. ¿Lista vac´ıa? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268 4.6.3. Inserci´on por cabeza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268 4.6.4. Longitud de una lista . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271 4.6.5. Impresi´on en pantalla . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272 4.6.6. Inserci´on por cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273 4.6.7. Borrado de la cabeza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275 4.6.8. Borrado de la cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276 4.6.9. B´usqueda de un elemento . . . . . . . . . . . . . . . . . . . . . . . . . . . 279 4.6.10. Borrado del primer nodo con un valor determinado . . . . . . . . . . . . . 280 4.6.11. Borrado de todos los nodos con un valor dado . . . . . . . . . . . . . . . . 282 4.6.12. Inserci´on en una posici´on dada . . . . . . . . . . . . . . . . . . . . . . . . 283 4.6.13. Inserci´on ordenada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285 4.6.14. Concatenaci´on de dos listas . . . . . . . . . . . . . . . . . . . . . . . . . . 286 4.6.15. Borrado de la lista completa . . . . . . . . . . . . . . . . . . . . . . . . . . 287 4.6.16. Juntando las piezas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 287 4.7. Listas simples con punteros a cabeza y cola . . . . . . . . . . . . . . . . . . . . . 294 4.7.1. Creaci´on de lista vac´ıa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 295 4.7.2. Inserci´on de nodo en cabeza . . . . . . . . . . . . . . . . . . . . . . . . . . 295 4.7.3. Inserci´on de nodo en cola . . . . . . . . . . . . . . . . . . . . . . . . . . . 296 4.7.4. Borrado de la cabeza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 297 4.7.5. Borrado de la cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 298 4.8. Listas con enlace doble . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300 4.8.1. Inserci´on por cabeza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300 4.8.2. Borrado de la cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 302 4.8.3. Inserci´on en una posici´on determinada . . . . . . . . . . . . . . . . . . . . 303 4.8.4. Borrado de la primera aparici´on de un elemento . . . . . . . . . . . . . . 305 4.9. Listas con enlace doble y puntero a cabeza y cola . . . . . . . . . . . . . . . . . . 307 4.10. Una gu´ıa para elegir listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 310 4.11. Una aplicaci´on: una base de datos para discos compactos . . . . . . . . . . . . . 311 4.12. Otras estructuras de datos con registros enlazados . . . . . . . . . . . . . . . . . 318 5. Ficheros 321 5.1. Ficheros de texto y ficheros binarios . . . . . . . . . . . . . . . . . . . . . . . . . 321 5.1.1. Representaci´on de la informaci´on en los ficheros de texto . . . . . . . . . . 321 5.1.2. Representaci´on de la informaci´on en los ficheros binarios . . . . . . . . . . 322 5.2. Ficheros de texto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 323 5.2.1. Abrir, leer/escribir, cerrar . . . . . . . . . . . . . . . . . . . . . . . . . . . 323 5.2.2. Aplicaciones: una agenda y un gestor de una colecci´on de discos compactos330 5.2.3. Los ((ficheros)) de consola . . . . . . . . . . . . . . . . . . . . . . . . . . . 339 Introducci´on a la Programaci´on con C iii
  • 6. ´INDICE GENERAL 5.2.4. Un par de utilidades . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 342 5.3. Ficheros binarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 344 5.3.1. Abrir, leer/escribir, cerrar . . . . . . . . . . . . . . . . . . . . . . . . . . . 344 5.3.2. Acceso directo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 349 5.4. Errores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353 A. Tipos b´asicos 355 A.1. Enteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355 A.1.1. Tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355 A.1.2. Literales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356 A.1.3. Marcas de formato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356 A.2. Flotantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356 A.2.1. Tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356 A.2.2. Literales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357 A.2.3. Marcas de formato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357 A.3. Caracteres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357 A.3.1. Literales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357 A.3.2. Marcas de formato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 358 A.4. Otros tipos b´asicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 358 A.4.1. El tipo booleano . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 358 A.4.2. Los tipos complejo e imaginario . . . . . . . . . . . . . . . . . . . . . . . . 358 A.5. Una reflexi´on acerca de la diversidad de tipos escalares . . . . . . . . . . . . . . . 358 B. La lectura de datos por teclado, paso a paso 359 B.1. La lectura de valores escalares con scanf . . . . . . . . . . . . . . . . . . . . . . . 359 B.2. La lectura de cadenas con scanf . . . . . . . . . . . . . . . . . . . . . . . . . . . 362 B.3. Un problema serio: la lectura alterna de cadenas con gets y de escalares con scanf 366 iv Introducci´on a la Programaci´on con C
  • 7. Cap´ıtulo 1 Introducci´on a C Hab´ıa un libro junto a Alicia, en la mesa; y mientras permanec´ıa sentada observando al Rey Blanco [. . . ], pasaba las hojas para ver si encontraba alg´un trozo que poder leer: ((. . . Porque est´a todo en una lengua que no entiendo)), se dijo. Estaba as´ı: JERIG´ONDOR Cocillabaeld´ıaylastovasagilimosas giroscopabanybarrenabanenellarde. Tododevirablesestabanlosburgovos, ysilbramabanlasalecasrastas. Durante un rato, estuvo contemplando esto perpleja; pero al final se le ocurri´o una brillante idea. ¡Ah, ya s´e!, ¡es un libro del Espejo, naturalmente! Si lo pongo delante de un espejo, las palabras se ver´an otra vez del derecho. Lewis Carroll, Alicia a trav´es del espejo. El lenguaje de programaci´on C es uno de los m´as utilizados (si no el que m´as) en la programaci´on de sistemas software. Es similar a Python en muchos aspectos fundamentales: presenta las mismas estructuras de control (selecci´on condicional, iteraci´on), permite trabajar con algunos tipos de datos similares (enteros, flotantes, secuencias), hace posible definir y usar funciones, etc. No obstante, en muchas otras cuestiones es un lenguaje muy diferente. C presenta ciertas caracter´ısticas que permiten ejercer un elevado control sobre la eficiencia de los programas, tanto en la velocidad de ejecuci´on como en el consumo de memoria, pero a un precio: tenemos que proporcionar informaci´on expl´ıcita sobre gran cantidad de detalles, por lo que generalmente resultan programas m´as largos y complicados que sus equivalentes en Python, aumentando as´ı la probabilidad de que cometamos errores. En este cap´ıtulo aprenderemos a realizar programas en C del mismo ((nivel)) que los que sab´ıamos escribir en Python tras estudiar el cap´ıtulo 4 del primer volumen. Aprenderemos, pues, a usar variables, expresiones, la entrada/salida, funciones definidas en ((m´odulos)) (que en C se denominan bibliotecas) y estructuras de control. Lo ´unico que dejamos pendiente de momento es el tratamiento de cadenas en C, que es sensiblemente diferente al que proporciona Python. Nada mejor que un ejemplo de programa en los dos lenguajes para que te lleves una primera impresi´on de cu´an diferentes son Python y C. . . y cu´an semejantes. Estos dos programas, el primero en Python y el segundo en C, calculan el valor de b i=a √ i para sendos valores enteros de a y b introducidos por el usuario y tales que 0 ≤ a ≤ b. Introducci´on a la Programaci´on con C 1
  • 8. Introducci´on a C sumatorio.py sumatorio.py 1 from math import * 2 3 # Pedir l´ımites inferior y superior. 4 a = int(raw_input(’L´ımite inferior:’)) 5 while a < 0: 6 print ’No puede ser negativo’ 7 a = int(raw_input(’L´ımite inferior:’)) 8 9 b = int(raw_input(’L´ımite superior:’)) 10 while b < a: 11 print ’No puede ser menor que %d’ % a 12 b = int(raw_input(’L´ımite superior:’)) 13 14 # Calcular el sumatorio de la ra´ız cuadrada de i para i entre a y b. 15 s = 0.0 16 for i in range(a, b+1): 17 s += sqrt(i) 18 19 # Mostrar el resultado. 20 print ’Sumatorio de ra´ıces’, 21 print ’de %d a %d: %f’ % (a, b, s) sumatorio.c sumatorio.c 1 #include <stdio.h> 2 #include <math.h> 3 4 int main(void) 5 { 6 int a, b, i; 7 float s; 8 9 /* Pedir l´ımites inferior y superior. */ 10 printf ("L´ımite inferior:"); 11 scanf ("%d", &a); 12 while (a < 0) { 13 printf ("No puede ser negativon"); 14 printf ("L´ımite inferior:"); 15 scanf ("%d", &a); 16 } 17 18 printf ("L´ımite superior:"); 19 scanf ("%d", &b); 20 while (b < a) { 21 printf ("No puede ser menor que %dn", a); 22 printf ("L´ımite superior:"); 23 scanf ("%d", &b); 24 } 25 26 /* Calcular el sumatorio de la ra´ız cuadrada de i para i entre a y b. */ 27 s = 0.0; 28 for (i = a; i <= b; i++) { 29 s += sqrt(i); 30 } 31 32 /* Mostrar el resultado. */ 33 printf ("Sumatorio de ra´ıces "); 34 printf ("de %d a %d: %fn", a, b, s); 35 36 return 0; 37 } En varios puntos de este cap´ıtulo haremos referencia a estos dos programas. No los pierdas 2 Introducci´on a la Programaci´on con C
  • 9. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C de vista. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 1 Compara los programas sumatorio.py y sumatorio.c. Analiza sus semejanzas y diferen- cias. ¿Qu´e funci´on desempe˜nan las llaves en sumatorio.c? ¿Qu´e funci´on crees que desempe˜nan las l´ıneas 6 y 7 del programa C? ¿A qu´e elemento de Python se parecen las dos primeras l´ıneas de sumatorio.c? ¿Qu´e similitudes y diferencias aprecias entre las estructuras de control de Python y C? ¿C´omo crees que se interpreta el bucle for del programa C? ¿Por qu´e algunas l´ıneas de sumatorio.c finalizan en punto y coma y otras no? ¿Qu´e diferencias ves entre los comentarios Python y los comentarios C? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Un poco de historia C ya tiene sus a˜nitos. El nacimiento de C est´a estrechamente vinculado al del sistema opera- tivo Unix. El investigador Ken Thompson, de AT&T, la compa˜n´ıa telef´onica estadounidense, se propuso dise˜nar un nuevo sistema operativo a principios de los setenta. Dispon´ıa de un PDP-7 en el que codific´o una primera versi´on de Unix en lenguaje ensamblador. Pronto se impuso la conveniencia de desarrollar el sistema en un lenguaje de programaci´on de alto nivel, pero la escasa memoria del PDP-7 (8K de 18 bits) hizo que ideara el lenguaje de programaci´on B, una versi´on reducida de un lenguaje ya existente: BCPL. El lenguaje C apareci´o como un B mejorado, fruto de las demandas impuestas por el desarrollo de Unix. Dennis Ritchie fue el encargado del dise˜no del lenguaje C y de la implementaci´on de un compilador para ´el sobre un PDP-11. C ha sufrido numerosos cambios a lo largo de su historia. La primera versi´on ((estable)) del lenguaje data de 1978 y se conoce como ((K&R C)), es decir, ((C de Kernighan y Ritchie)). Esta versi´on fue descrita por sus autores en la primera edici´on del libro ((The C Programming Language)) (un aut´entico ((best-seller)) de la inform´atica). La adopci´on de Unix como siste- ma operativo de referencia en las universidades en los a˜nos 80 populariz´o enormemente el lenguaje de programaci´on C. No obstante, C era atractivo por s´ı mismo y parec´ıa satisfacer una demanda real de los programadores: disponer de un lenguaje de alto nivel con ciertas caracter´ısticas propias de los lenguajes de bajo nivel (de ah´ı que a veces se diga que C es un lenguaje de nivel intermedio). La experiencia con lenguajes de programaci´on dise˜nados con anterioridad, como Lisp o Pascal, demuestra que cuando el uso de un lenguaje se extiende es muy probable que proli- feren variedades dialectales y extensiones para aplicaciones muy concretas, lo que dificulta enormemente el intercambio de programas entre diferentes grupos de programadores. Para evitar este problema se suele recurrir a la creaci´on de un comit´e de expertos que define la versi´on oficial del lenguaje. El comit´e ANSI X3J9 (ANSI son las siglas del American National Standards Institute), creado en 1983, considera la inclusi´on de aquellas extensiones y mejo- ras que juzga de suficiente inter´es para la comunidad de programadores. El 14 de diciembre de 1989 se acord´o qu´e era el ((C est´andar)) y se public´o el documento con la especificaci´on en la primavera de 1990. El est´andar se divulg´o con la segunda edici´on de ((The C Pro- gramming Language)), de Brian Kernighan y Dennis Ritchie. Un comit´e de la International Standards Office (ISO) ratific´o el documento del comit´e ANSI en 1992, convirti´endolo as´ı en un est´andar internacional. Durante mucho tiempo se conoci´o a esta versi´on del lenguaje como ANSI-C para distinguirla as´ı del K&R C. Ahora se prefiere denominar a esta variante C89 (o C90) para distinguirla de la revisi´on que se public´o en 1999, la que se conoce por C99 y que es la versi´on est´andar de C que estudiaremos. C ha tenido un gran impacto en el dise˜no de otros muchos lenguajes. Ha sido, por ejemplo, la base para definir la sintaxis y ciertos aspectos de la sem´antica de lenguajes tan populares como Java y C++. 1.1. C es un lenguaje compilado Python y C no s´olo se diferencian en su sintaxis, tambi´en son distintos en el modo en que se traducen los programas a c´odigo de m´aquina y en el modo en que ejecutamos los programas. Python es un lenguaje interpretado: para ejecutar un programa Python, suministramos al int´erprete un fichero de texto (t´ıpicamente con extensi´on ((.py))) con su c´odigo fuente. Si deseamos ejecutar sumatorio.py, por ejemplo, hemos de escribir python sumatorio.py Introducci´on a la Programaci´on con C 3
  • 10. 1.1 C es un lenguaje compilado en la l´ınea de ´ordenes Unix. Como resultado, el int´erprete va leyendo y ejecutando paso a paso el programa. Para volver a ejecutarlo, has de volver a escribir python sumatorio.py en la l´ınea de ´ordenes, con lo que se repite el proceso completo de traducci´on y ejecuci´on paso a paso. Aunque no modifiquemos el c´odigo fuente, es necesario interpretarlo (traducir y ejecutar paso a paso) nuevamente. sumatorio.py Int´erprete Python Resultados C es un lenguaje compilado: antes de ejecutar un programa escrito por nosotros, sumi- nistramos su c´odigo fuente (en un fichero con extensi´on ((.c))) a un compilador de C. El compilador lee y analiza todo el programa. Si el programa est´a correctamente escrito seg´un la definici´on del lenguaje, el compilador genera un nuevo fichero con su traducci´on a c´odigo de m´aquina, y si no, muestra los errores que ha detectado. Para ejecutar el pro- grama utilizamos el nombre del fichero generado. Si no modificamos el c´odigo fuente, no hace falta que lo compilemos nuevamente para volver a ejecutar el programa: basta con volver a ejecutar el fichero generado por el compilador. Para ejecutar sumatorio.c, por ejemplo, primero hemos de usar un compilador para producir un nuevo fichero llamado sumatorio. sumatorio.c Compilador de C sumatorio Podemos ejecutar el programa escribiendo sumatorio en la l´ınea de ´ordenes Unix.1 sumatorio Resultados Si queremos volver a ejecutarlo, basta con escribir de nuevo sumatorio; no es necesario volver a compilar el contenido del fichero sumatorio.c. sumatorio Resultados La principal ventaja de compilar los programas es que se gana en velocidad de ejecuci´on, ya que cuando el programa se ejecuta est´a completamente traducido a c´odigo de m´aquina y se ahorra el proceso de ((traducci´on simult´anea)) que conlleva interpretar un programa. Pero, adem´as, como se traduce a c´odigo de m´aquina en una fase independiente de la fase de ejecuci´on, el programa traductor puede dedicar m´as tiempo a intentar encontrar la mejor traducci´on posible, la que proporcione el programa de c´odigo de m´aquina m´as r´apido (o que consuma menos memoria). Nosotros usaremos un compilador concreto de C: gcc (en su versi´on 3.2 o superior)2 . Su forma de uso m´as b´asica es ´esta: gcc fichero.c -o fichero ejecutable La opci´on -o es abreviatura de ((output)), es decir, ((salida)), y a ella le sigue el nombre del fichero que contendr´a la traducci´on a c´odigo m´aquina del programa. Debes tener presente que dicho fichero s´olo se genera si el programa C est´a correctamente escrito. Si queremos compilar el programa sumatorio.c hemos de usar una opci´on especial: gcc sumatorio.c -lm -o sumatorio La opci´on -lm se debe usar siempre que nuestro programa utilice funciones del m´odulo matem´atico (como sqrt, que se usa en sumatorio.c). Ya te indicaremos por qu´e en la secci´on dedicada a presentar el m´odulo matem´atico de C. 1Por razones de seguridad es probable que no baste con escribir sumatorio para poder ejecutar un programa con ese nombre y que reside en el directorio activo. Si es as´ı, prueba con ./sumatorio. 2La versi´on 3.2 de gcc es la primera en ofrecer un soporte suficiente de C99. Si usas una versi´on anterior, es posible que algunos (pocos) programas del libro no se compilen correctamente. 4 Introducci´on a la Programaci´on con C
  • 11. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C C99 y gcc Por defecto, gcc acepta programas escritos en C89 con extensiones introducidas por GNU (el grupo de desarrolladores de muchas herramientas de Linux). Muchas de esas extensiones de GNU forman ya parte de C99, as´ı que gcc es, por defecto, el compilador de un lenguaje intermedio entre C89 y C99. Si en alg´un momento da un aviso indicando que no puede compilar alg´un programa porque usa caracter´ısticas propias del C99 no disponibles por defecto, puedes forzarle a compilar en ((modo C99)) as´ı: gcc programa.c -std=c99 -o programa Has de saber, no obstante, que gcc a´un no soporta el 100% de C99 (aunque s´ı todo lo que te explicamos en este texto). El compilador gcc acepta muchas otras variantes de C. Puedes forzarle a aceptar una en particular ((asignando)) a la opci´on -std el valor c89, c99, gnu89 o gnu99. 1.2. Traduciendo de Python a C: una gu´ıa r´apida Empezaremos por presentar de forma concisa c´omo traducir la mayor parte de los programas Python que aprendimos a escribir en los cap´ıtulos 3 y 4 del primer volumen a programas equi- valentes en C. En secciones posteriores entraremos en detalle y nos dedicaremos a estudiar las muchas posibilidades que ofrece C a la hora de seleccionar tipos de datos, presentar informaci´on con sentencias de impresi´on en pantalla, etc. 1. Los programas (sencillos) presentan, generalmente, este aspecto: 1 #include <stdio.h> 2 3 Posiblemente otros ((#include)) 4 5 int main(void) 6 { 7 Programa principal. 8 9 return 0; 10 } Hay, pues, dos zonas: una inicial cuyas l´ıneas empiezan por #include (equivalentes a las sentencias import de Python) y una segunda que empieza con una l´ınea ((int main(void))) y comprende las sentencias del programa principal mas una l´ınea ((return 0;)), encerradas todas ellas entre llaves ({ y }). De ahora en adelante, todo texto comprendido entre llaves recibir´a el nombre de bloque. 2. Toda variable debe declararse antes de ser usada. La declaraci´on de la variable consiste en escribir el nombre de su tipo (int para enteros y float para flotantes)3 seguida del identificador de la variable y un punto y coma. Por ejemplo, si vamos a usar una variable entera con identificador a y una variable flotante con identificador b, nuestro programa las declarar´a as´ı: 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a; 6 float b; 7 8 Sentencias donde se usan las variables. 9 10 return 0; 11 } 3Recuerda que no estudiaremos las variables de tipo cadena hasta el pr´oximo cap´ıtulo. Introducci´on a la Programaci´on con C 5
  • 12. 1.2 Traduciendo de Python a C: una gu´ıa r´apida No es obligatorio que la declaraci´on de las variables tenga lugar justo al principio del bloque que hay debajo de la l´ınea ((int main(void))), pero s´ı conveniente.4 Si tenemos que declarar dos o m´as variables del mismo tipo, podemos hacerlo en una misma l´ınea separando los identificadores con comas. Por ejemplo, si las variables x, y y z son todas de tipo float, podemos recurrir a esta forma compacta de declaraci´on: 1 #include <stdio.h> 2 3 int main(void) 4 { 5 float x, y, z; 6 7 ... 8 9 return 0; 10 } 3. Las sentencias de asignaci´on C son similares a las sentencias de asignaci´on Python: a mano izquierda del s´ımbolo igual (=) se indica la variable a la que se va a asignar el valor que resulta de evaluar la expresi´on que hay a mano derecha. Cada sentencia de asignaci´on debe finalizar con punto y coma. 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a; 6 float b; 7 8 a = 2; 9 b = 0.2; 10 11 return 0; 12 } Como puedes ver, los n´umeros enteros y flotantes se representan igual que en Python. 4. Las expresiones se forman con los mismos operadores que aprendimos en Python. Bueno, hay un par de diferencias: Los operadores Python and, or y not se escriben en C, respectivamente, con &&, || y !; No hay operador de exponenciaci´on (que en Python era **). Hay operadores para la conversi´on de tipos. Si en Python escrib´ıamos float(x) para convertir el valor de x a flotante, en C escribiremos (float) x para expresar lo mismo. F´ıjate en c´omo se disponen los par´entesis: los operadores de conversi´on de tipos son de la forma (tipo). 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a; 6 float b; 7 8 a = 13 % 2; 9 b = 2.0 / (1.0 + 2 - (a + 1)); 10 11 return 0; 12 } 4En versiones de C anteriores a C99 s´ı era obligatorio que las declaraciones se hicieran al principio de un bloque. C99 permite declarar una variable en cualquier punto del programa, siempre que ´este sea anterior al primer uso de la misma. 6 Introducci´on a la Programaci´on con C
  • 13. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C Las reglas de asociatividad y precedencia de los operadores son casi las mismas que apren- dimos en Python. Hay m´as operadores en C y los estudiaremos m´as adelante. 5. Para mostrar resultados por pantalla se usa la funci´on printf . La funci´on recibe uno o m´as argumentos separados por comas: primero, una cadena con formato, es decir, con marcas de la forma %d para re- presentar enteros y marcas %f para representar flotantes (en los que podemos usar modificadores para, por ejemplo, controlar la cantidad de espacios que ocupar´a el valor o la cantidad de cifras decimales de un n´umero flotante); y, a continuaci´on, las expresiones cuyos valores se desea mostrar (debe haber una expresi´on por cada marca de formato). escribe.c escribe.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a; 6 float b; 7 8 a = 13 % 2; 9 b = 2.0 / (1.0 + 2 - (a + 1)); 10 11 printf ("El valor de a es %d y el de b es %fn", a, b); 12 13 return 0; 14 } La cadena con formato debe ir encerrada entre comillas dobles, no simples. El car´acter de retorno de carro (n) es obligatorio si se desea finalizar la impresi´on con un salto de l´ınea. (Observa que, a diferencia de Python, no hay operador de formato entre la cadena de formato y las expresiones: la cadena de formato se separa de la primera expresi´on con una simple coma). Como puedes ver, todas las sentencias de los programas C que estamos presentando fina- lizan con punto y coma. 6. Para leer datos de teclado has de usar la funci´on scanf . F´ıjate en este ejemplo: lee y escribe.c lee y escribe.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a; 6 float b; 7 8 scanf ("%d", &a); 9 scanf ("%f", &b); 10 11 printf ("El valor de a es %d y el de b es %fn", a, b); 12 13 return 0; 14 } La l´ınea 8 lee de teclado el valor de un entero y lo almacena en a. La l´ınea 9 lee de teclado el valor de un flotante y lo almacena en b. Observa el uso de marcas de formato en el primer argumento de scanf : %d se˜nala la lectura de un int y %f la de un float. El s´ımbolo & que precede al identificador de la variable en la que se almacena el valor le´ıdo es obligatorio para variables de tipo escalar. Si deseas mostrar por pantalla un texto que proporcione informaci´on acerca de lo que el usuario debe introducir, hemos de usar nuevas sentencias printf : Introducci´on a la Programaci´on con C 7
  • 14. 1.2 Traduciendo de Python a C: una gu´ıa r´apida lee mejor y escribe.c lee mejor y escribe.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a; 6 float b; 7 8 printf ("Introduce un entero a: "); 9 scanf ("%d", &a); 10 printf ("Y ahora un flotante b: "); 11 scanf ("%f", &b); 12 13 printf ("El valor de a es %d y el de b es %fn", a, b); 14 15 return 0; 16 } 7. La sentencia if de Python presenta un aspecto similar en C: si es par.c si es par.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a; 6 7 printf ("Introduce un entero a: "); 8 scanf ("%d", &a); 9 10 if (a % 2 == 0) { 11 printf ("El valor de a es par.n"); 12 printf ("Es curioso.n"); 13 } 14 15 return 0; 16 } Ten en cuenta que: la condici´on va encerrada obligatoriamente entre par´entesis; y el bloque de sentencias cuya ejecuci´on est´a supeditada a la satisfacci´on de la con- dici´on va encerrado entre llaves (aunque matizaremos esta afirmaci´on m´as adelante). Naturalmente, puedes anidar sentencias if. si es par y positivo.c si es par y positivo.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a; 6 7 printf ("Introduce un entero a: "); 8 scanf ("%d", &a); 9 10 if (a % 2 == 0) { 11 printf ("El valor de a es par.n"); 12 if (a > 0) { 13 printf ("Y, adem´as, es positivo.n"); 14 } 15 } 16 8 Introducci´on a la Programaci´on con C
  • 15. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C 17 return 0; 18 } Tambi´en hay sentencia if-else en C: par o impar.c par o impar.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a; 6 7 printf ("Introduce un entero a: "); 8 scanf ("%d", &a); 9 10 if (a % 2 == 0) { 11 printf ("El valor de a es par.n"); 12 } 13 else { 14 printf ("El valor de a es impar.n"); 15 } 16 17 return 0; 18 } No hay, sin embargo, sentencia if-elif, aunque es f´acil obtener el mismo efecto con una sucesi´on de if-else if: tres casos.c tres casos.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a; 6 7 printf ("Introduce un entero a: "); 8 scanf ("%d", &a); 9 10 if (a > 0) { 11 printf ("El valor de a es positivo.n"); 12 } 13 else if (a == 0) { 14 printf ("El valor de a es nulo.n"); 15 } 16 else if (a < 0) { 17 printf ("El valor de a es negativo.n"); 18 } 19 else { 20 printf ("Es imposible mostrar este mensaje.n"); 21 } 22 23 return 0; 24 } 8. La sentencia while de C es similar a la de Python, pero has de tener en cuenta la obligatoriedad de los par´entesis alrededor de la condici´on y que las sentencias que se pueden repetir van encerradas entre un par de llaves: cuenta atras.c cuenta atras.c 1 #include <stdio.h> 2 3 int main(void) 4 { Introducci´on a la Programaci´on con C 9
  • 16. 1.2 Traduciendo de Python a C: una gu´ıa r´apida 5 int a; 6 7 printf ("Introduce un entero a: "); 8 scanf ("%d", &a); 9 10 while (a > 0) { 11 printf ("%d", a); 12 a -= 1; 13 } 14 printf (" ! Boom!n"); 15 16 return 0; 17 } 9. Tambi´en puedes usar la sentencia break en C: primo.c primo.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a, b; 6 7 printf ("Introduce un entero a: "); 8 scanf ("%d", &a); 9 10 b = 2; 11 while (b < a) { 12 if (a % b == 0) { 13 break; 14 } 15 b += 1; 16 } 17 if (b == a) { 18 printf ("%d es primo.n", a); 19 } 20 else { 21 printf ("%d no es primo.n", a); 22 } 23 24 return 0; 25 } 10. Los m´odulos C reciben el nombre de bibliotecas y se importan con la sentencia #include. Ya hemos usado #include en la primera l´ınea de todos nuestros programas: #include <stdio.h>. Gracias a ella hemos importado las funciones de entrada/salida scanf y printf . No se puede importar una sola funci´on de una biblioteca: debes importar el contenido completo de la biblioteca. Las funciones matem´aticas pueden importarse del m´odulo matem´atico con #include <math.h> y sus nombres son los mismos que vimos en Python (sin para el seno, cos para el coseno, etc.). raiz cuadrada.c raiz cuadrada.c 1 #include <stdio.h> 2 #include <math.h> 3 4 int main(void) 5 { 6 float b; 7 8 printf ("Escribe un flotante: "); 9 scanf ("%f", &b); 10 Introducci´on a la Programaci´on con C
  • 17. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C 10 11 if (b >= 0.0) { 12 printf ("Su ra´ız cuadrada es %f.n", sqrt(b)); 13 } 14 else { 15 printf ("No puedo calcular su ra´ız cuadrada.n"); 16 } 17 18 return 0; 19 } No hay funciones predefinidas en C. Muchas de las que estaban predefinidas en Python pueden usarse en C, pero import´andolas de bibliotecas. Por ejemplo, abs (valor absolu- to) puede importarse del m´odulo stdlib.h (por ((standard library)), es decir, ((biblioteca est´andar))). Las (aproximaciones a las) constantes π y e se pueden importar de la biblioteca ma- tem´atica, pero sus identificadores son ahora M_PI y M_E, respectivamente. No est´a mal: ya sabes traducir programas Python sencillos a C (aunque no sabemos traducir programas con definiciones de funci´on, ni con variables de tipo cadena, ni con listas, ni con registros, ni con acceso a ficheros. . . ). ¿Qu´e tal practicar con unos pocos ejercicios? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 2 Traduce a C este programa Python. 1 a = int(raw_input(’Dame el primer n´umero: ’)) 2 b = int(raw_input(’Dame el segundo n´umero: ’)) 3 4 if a >= b: 5 maximo = a 6 else: 7 maximo = b 8 9 print ’El m´aximo es’, maximo · 3 Traduce a C este programa Python. 1 n = int(raw_input(’Dame un n´umero: ’)) 2 m = int(raw_input(’Dame otro n´umero: ’)) 3 4 if n * m == 100: 5 print ’El producto %d * %d es igual a 100’ % (n, m) 6 else: 7 print ’El producto %d * %d es distinto de 100’ % (n, m) · 4 Traduce a C este programa Python. 1 from math import sqrt 2 3 x1 = float(raw_input("Punto 1, coordenada x: ")) 4 y1 = float(raw_input("Punto 1, coordenada y: ")) 5 x2 = float(raw_input("Punto 2, coordenada x: ")) 6 y2 = float(raw_input("Punto 2, coordenada y: ")) 7 dx = x2 - x1 8 dy = y2 - y1 9 distancia = sqrt(dx**2 + dy**2) 10 print ’La distancia entre los puntos es: ’, distancia · 5 Traduce a C este programa Python. 1 a = float(raw_input(’Valor de a: ’)) 2 b = float(raw_input(’Valor de b: ’)) 3 4 if a != 0: 5 x = -b/a Introducci´on a la Programaci´on con C 11
  • 18. 1.3 Estructura t´ıpica de un programa C 6 print ’Soluci´on: ’, x 7 else: 8 if b != 0: 9 print ’La ecuaci´on no tiene soluci´on.’ 10 else: 11 print ’La ecuaci´on tiene infinitas soluciones.’ · 6 Traduce a C este programa Python. 1 from math import log 2 3 x = 1.0 4 while x < 10.0: 5 print x, ’t’, log(x) 6 x = x + 1.0 · 7 Traduce a C este programa Python. 1 n = 1 2 while n < 6: 3 i = 1 4 while i < 6: 5 print n*i, ’t’, 6 i = i + 1 7 print 8 n = n + 1 · 8 Traduce a C este programa Python. 1 from math import pi 2 3 opcion = 0 4 while opcion != 4: 5 print ’Escoge una opci´on: ’ 6 print ’1) Calcular el di´ametro.’ 7 print ’2) Calcular el per´ımetro.’ 8 print ’3) Calcular el ´area.’ 9 print ’4) Salir.’ 10 opcion = int(raw_input(’Teclea 1, 2, 3 o 4 y pulsa el retorno de carro: ’)) 11 12 radio = float(raw_input(’Dame el radio de un c´ırculo: ’)) 13 14 if opcion == 1: 15 diametro = 2 * radio 16 print ’El di´ametro es’, diametro 17 elif opcion == 2: 18 perimetro = 2 * pi * radio 19 print ’El per´ımetro es’, perimetro 20 elif opcion == 3: 21 area = pi * radio ** 2 22 print ’El ´area es’, area 23 elif opcion < 0 or opcion > 4: 24 print ’S´olo hay cuatro opciones: 1, 2, 3 o 4. T´u has tecleado’, opcion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ya es hora, pues, de empezar con los detalles de C. 1.3. Estructura t´ıpica de un programa C Un programa C no es m´as que una colecci´on de declaraciones de variables globales y de defini- ciones de constantes, macros, tipos y funciones. Una de las funciones es especial: se llama main (que en ingl´es significa ((principal))) y contiene el c´odigo del programa principal. No nos deten- dremos a explicar la sintaxis de la definici´on de funciones hasta el cap´ıtulo 3, pero debes saber ya que la definici´on de la funci´on main empieza con ((int main (void))) y sigue con el cuerpo de la funci´on encerrado entre un par de llaves. La funci´on main debe devolver un valor entero 12 Introducci´on a la Programaci´on con C
  • 19. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C al final (t´ıpicamente el valor 0), por lo que finaliza con una sentencia return que devuelve el valor 0.5 La estructura t´ıpica de un programa C es ´esta: Importaci´on de funciones, variables, constantes, etc. Definici´on de constantes y macros. Definici´on de nuevos tipos de datos. Declaraci´on de variables globales. Definici´on de funciones. int main(void) { Declaraci´on de variables propias del programa principal (o sea, locales a main). Programa principal. return 0; } Un fichero con extensi´on ((.c)) que no define la funci´on main no es un programa C completo. Si, por ejemplo, tratamos de compilar este programa incorrecto (no define main): E sin main.c E 1 int a; 2 a = 1; el compilador muestra el siguiente mensaje (u otro similar, seg´un la versi´on del compilador que utilices): $ gcc sin_main.c -o sin_main sin_main.c:2: warning: data definition has no type or storage class /usr/lib/crt1.o: En la funci´on ‘_start’: /usr/lib/crt1.o(.text+0x18): referencia a ‘main’ sin definir collect2: ld returned 1 exit status F´ıjate en la tercera l´ınea del mensaje de error: ((referencia a ‘main’ sin definir)). 1.4. C es un lenguaje de formato libre As´ı como en Python la indentaci´on determina los diferentes bloques de un programa, en C la indentaci´on es absolutamente superflua: indentamos los programas ´unicamente para hacerlos m´as legibles. En C se sabe d´onde empieza y d´onde acaba un bloque porque ´este est´a encerrado entre una llave abierta ({) y otra cerrada (}). He aqu´ı un ejemplo de bloques anidados en el que hemos indentado el c´odigo para facilitar su lectura: minimo.c minimo.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a, b, c, minimo; 6 7 scanf ("%d", &a); 8 scanf ("%d", &b); 9 scanf ("%d", &c); 5El valor 0 se toma, por un convenio, como se˜nal de que el programa finaliz´o correctamente. El sistema operativo Unix recibe el valor devuelto con el return y el int´erprete de ´ordenes, por ejemplo, puede tomar una decisi´on acerca de qu´e hacer a continuaci´on en funci´on del valor devuelto. Introducci´on a la Programaci´on con C 13
  • 20. 1.4 C es un lenguaje de formato libre 10 if (a < b) { 11 if (a < c) { 12 minimo = a; 13 } 14 else { 15 minimo = c; 16 } 17 } 18 else { 19 if (b < c) { 20 minimo = b; 21 } 22 else { 23 minimo = c; 24 } 25 } 26 printf ("%dn", minimo); 27 return 0; 28 } Este programa podr´ıa haberse escrito como sigue y ser´ıa igualmente correcto: minimo 1.c minimo.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a, b, c, minimo; 6 7 scanf ("%d", &a); 8 scanf ("%d", &b); 9 scanf ("%d", &c); 10 if (a < b) { 11 if (a < c) { minimo = a; } 12 else { minimo = c; } 13 } 14 else { 15 if (b < c) { minimo = b; } 16 else { minimo = c; } 17 } 18 printf ("%dn", minimo); 19 return 0; 20 } Cuando un bloque consta de una sola sentencia no es necesario encerrarla entre llaves. Aqu´ı tienes un ejemplo: minimo 2.c minimo.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a, b, c, minimo; 6 7 scanf ("%d", &a); 8 scanf ("%d", &b); 9 scanf ("%d", &c); 10 if (a < b) { 11 if (a < c) minimo = a; 12 else minimo = c; 13 } 14 else { 15 if (b < c) minimo = b; 16 else minimo = c; 14 Introducci´on a la Programaci´on con C
  • 21. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C 17 } 18 printf ("%dn", minimo); 19 return 0; 20 } De hecho, como if-else es una ´unica sentencia, tambi´en podemos suprimir las llaves restantes: minimo 3.c minimo.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a, b, c, minimo; 6 7 scanf ("%d", &a); 8 scanf ("%d", &b); 9 scanf ("%d", &c); 10 if (a < b) 11 if (a < c) minimo = a; 12 else minimo = c; 13 else 14 if (b < c) minimo = b; 15 else minimo = c; 16 printf ("%dn", minimo); 17 return 0; 18 } Debes tener cuidado, no obstante, con las ambig¨uedades que parece producir un s´olo else y dos if: primero es minimo 1.c E primero es minimo.c E 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a, b, c, minimo; 6 7 scanf ("%d", &a); 8 scanf ("%d", &b); 9 scanf ("%d", &c); 10 if (a < b) 11 if (a < c) 12 printf ("El primero es el m´ınimo.n"); 13 else 14 printf ("El primero no menor que el segundo.n"); 15 printf ("%dn", minimo); 16 return 0; 17 } ¿Cu´al de los dos if se asocia al else? C usa una regla: el else se asocia al if m´as pr´oximo (en el ejemplo, el segundo). Seg´un esa regla, el programa anterior no es correcto. El sangrado sugiere una asociaci´on entre el primer if y el else que no es la que interpreta C. Para que C ((entienda)) la intenci´on del autor es necesario que explicites con llaves el alcance del primer if: primero es minimo 2.c primero es minimo.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a, b, c, minimo; 6 7 scanf ("%d", &a); 8 scanf ("%d", &b); 9 scanf ("%d", &c); 10 if (a < b) { Introducci´on a la Programaci´on con C 15
  • 22. 1.4 C es un lenguaje de formato libre 11 if (a < c) 12 printf ("El primero es el m´ınimo.n"); 13 } 14 else 15 printf ("El primero no es menor que el segundo.n"); 16 printf ("%dn", minimo); 17 return 0; 18 } Ahora que has adquirido la pr´actica de indentar los programas gracias a la disciplina im- puesta por Python, s´ıguela siempre, aunque programes en C y no sea necesario. La indentaci´on no importa. . . pero nadie se pone de acuerdo En C no es obligatorio indentar los programas, aunque todos los programadores est´an de acuerdo en que un programa sin una ((correcta)) indentaci´on es ilegible. ¡Pero no hay consenso en lo que significa indentar ((correctamente))! Hay varios estilos de indentaci´on en C y cada grupo de desarrolladores escoge el que m´as le gusta. Te presentamos unos pocos estilos: a) La llave abierta se pone en la misma l´ınea con la estructura de control y la llave de cierre va en una l´ınea a la altura del inicio de la estructura: if (a==1) { b = 1; c = 2; } b) ´Idem, pero la llave de cierre se dispone un poco a la derecha: if (a==1) { b = 1; c = 2; } c) La llave abierta va en una l´ınea sola, al igual que la llave cerrada. Ambas se disponen a la altura de la estructura que gobierna el bloque: if (a==1) { b = 1; c = 2; } d) ´Idem, pero las dos llaves se disponen m´as a la derecha y el contenido del bloque m´as a la derecha: if (a==1) { b = 1; c = 2; } e) Y a´un otro, con las llaves a la misma altura que el contenido del bloque: if (a==1) { b = 1; c = 2; } No hay un estilo mejor que otro. Es cuesti´on de puro convenio. A´un as´ı, hay m´as de una discusi´on subida de tono en los grupos de debate para desarrolladores de C. Incre´ıble, ¿no? En este texto hemos optado por el primer estilo de la lista (que, naturalmente, es el ((correcto)) ;-)) para todas las construcciones del lenguaje a excepci´on de la definici´on de funciones (como main), que sigue el convenio de indentaci´on que relacionamos en tercer lugar. Una norma: las sentencias C acaban con un punto y coma. Y una excepci´on a la norma: no 16 Introducci´on a la Programaci´on con C
  • 23. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C hace falta poner punto y coma tras una llave cerrada.6 Dado que las sentencias finalizan con punto y coma, no tienen por qu´e ocupar una l´ınea. Una sentencia como ((a = 1;)) podr´ıa escribirse, por ejemplo, en cuatro l´ıneas: a = 1 ; Pero aunque sea l´ıcito escribir as´ı esa sentencia, no tienen ning´un sentido y hace m´as dif´ıcil la comprensi´on del programa. Recuerda: vela siempre por la legibilidad de los programas. Tambi´en podemos poner m´as de una sentencia en una misma l´ınea, pues el compilador sabr´a d´onde empieza y acaba cada una gracias a los puntos y comas, las llaves, etc. El programa sumatorio.c, por ejemplo, podr´ıa haberse escrito as´ı: sumatorio ilegible.c sumatorio ilegible.c 1 #include <stdio.h> 2 #include <math.h> 3 int main(void) { int a, b, i; float s; /* Pedir l´ımites inferior y superior. */ printf ( 4 "L´ımite inferior:"); scanf ("%d", &a); while (a < 0) { printf ("No puede ser negativon"); 5 printf ("L´ımite inferior:"); scanf ("%d", &a); } printf ("L´ımite superior:"); scanf ("%d", 6 &b); while (b < a) { printf ("No puede ser mayor que %dn", a); printf ("L´ımite superior:") 7 ; scanf ("%d", &b); } /* Calcular el sumatorio de la ra´ız cuadrada de i para i entre a y b. */ s = 8 0.0; for (i = a; i <= b; i++) { s += sqrt(i); } /* Mostrar el resultado. */ printf ( 9 "Sumatorio de ra´ıces "); printf ("de %d a %d: %fn", a, b, s); return 0;} Obviamente, hubiera sido una mala elecci´on: un programa escrito as´ı, aunque correcto, es com- pletamente ilegible.7 Un programador de C experimentado hubiera escrito sumatorio.c utilizando llaves s´olo don- de resultan necesarias y, probablemente, utilizando unas pocas l´ıneas menos. Estudia las dife- rencias entre la primera versi´on de sumatorio.c y esta otra: sumatorio 1.c sumatorio.c 1 #include <stdio.h> 2 #include <math.h> 3 4 int main(void) 5 { 6 int a, b, i; 7 float s; 8 9 /* Pedir l´ımites inferior y superior. */ 10 printf ("L´ımite inferior:"); scanf ("%d", &a); 11 while (a < 0) { 12 printf ("No puede ser negativonL´ımite inferior:"); scanf ("%d", &a); 13 } 14 15 printf ("L´ımite superior:"); scanf ("%d", &b); 16 while (b < a) { 17 printf ("No puede ser mayor que %dn L´ımite superior:", a); scanf ("%d", &b); 18 } 19 20 /* Calcular el sumatorio de la ra´ız cuadrada de i para i entre a y b. */ 21 s = 0.0; 22 for (i = a; i <= b; i++) s += sqrt(i); 23 24 /* Mostrar el resultado. */ 25 printf ("Sumatorio de ra´ıces de %d a %d: %fn", a, b, s); 26 27 return 0; 28 } 6Habr´a una excepci´on a esta norma: las construcciones struct, cuya llave de cierre debe ir seguida de un punto y coma. 7Quiz´a hayas reparado en que las l´ıneas que empiezan con #include son especiales y que las tratamos de forma diferente: no se puede jugar con su formato del mismo modo que con las dem´as: cada sentencia #include debe ocupar una l´ınea y el car´acter # debe ser el primero de la l´ınea. Introducci´on a la Programaci´on con C 17
  • 24. 1.4 C es un lenguaje de formato libre International Obfuscated C Code Contest Es posible escribir programas ilegibles en C, ¡hasta tal punto que hay un concurso inter- nacional de programas ilegibles escritos en C!: el International Obfuscated C Code Contest (IOCCC). Aqu´ı tienes un programa C (en K&R C, ligeramente modificado para que pueda compilarse con gcc) que concurs´o en 1989: extern int errno ;char grrr ;main( r, argv, argc ) int argc , r ; char *argv[];{int P( ); #define x int i=0, j=0,cc[4];printf(" choo choon" ) ; x ;if (P( ! i ) | cc[ ! j ] & P(j )>2 ? j : i ){* argv[i++ +!-i] ; for (i= 0;; i++ ); _exit(argv[argc- 2 / cc[1*argc]|-1<<4 ] ) ;printf("%d",P(""));}} P ( a ) char a ; { a ; while( a > " B " /* - by E ricM arsh all- */); } ¿Sabes qu´e hace? ¡S´olo imprime en pantalla ((choo choo))! El siguiente programa es un generador de anagramas escrito por Andreas Gustafsson (AG ;-)) y se present´o a la edici´on de 1992: #include <stdio.h> long a [4],b[ 4],c[4] ,d[0400],e=1; typedef struct f{long g ,h,i[4] ,j;struct f*k;}f;f g,* l[4096 ]; char h[256],*m,k=3; long n (o, p,q)long*o,*p,*q;{ long r =4,s,i=0;for(;r--;s=i^ *o^*p, i=i&*p|(i|*p)&~*o++,*q ++=s,p ++);return i;}t(i,p)long*p ;{*c=d [i],n(a,c,b),n(p,b,p);}u(j)f*j;{j->h =(j->g =j->i[0]|j->i[1]|j->i[2]|j->i[3])&4095;}v( j,s)f* j; {int i; for(j->k->k&&v(j->k, ’ ’),fseek( stdin, j->j, 0);i=getchar(),putchar(i-’n’?i:s),i- ’n’;);}w(o,r,j,x,p)f*o,*j;long p;{f q;int s,i=o->h;q.k=o;r>i?j=l[r=i]:r<i&& (s=r&~i)?(s|=s>>1, s|=s >>2,s|=s>>4,s |=s>>8 ,j=l[r =((r&i |s)&~(s>>1))-1&i]):0;--x;for (;x&&!(p&i);p>>=1);for(;!x&&j;n(o->i,j->i,q. i),u(&q),q.g||(q.j=j->j,v(&q,’n’)),j=j->k);for(;x;j=x ?j->k:0){for(;!j&&((r=(r&i)-1&i)-i&&(r&p)?2:(x=0));j=l[r]);! x||(j->g&~o->g)||n (o->i,j->i,q.i)||( u(&q), q.j=j ->j,q.g?w(&q ,r,j->k,x ,p):v(&q, ’n’)); }}y(){f j;char *z,*p; for(;m ? j.j= ftell( stdin) ,7,(m= gets(m ))||w( &g,315 *13,l[ 4095] ,k,64* 64)&0: 0;n(g .i,j.i, b)||(u (&j),j. k=l[j.h],l[j.h]= &j,y())){for(z= p=h;*z&&( d[*z++]||(p=0)););for(z=p?n(j.i ,j.i,j.i)+h:""; *z;t(*z++,j.i));}}main(o,p)char** p; {for(;m = *++p;)for(;*m- ’-’?*m:(k= -atoi(m))&0;d[*m]||(d[*m ]=e,e<<=1),t(*m++,g.i)); u(& g),m=h ,y();} El programa lee un diccionario de la entrada est´andar y recibe como argumentos el n´umero de palabras del anagrama (precedido por un gui´on) y el texto del que se desea obtener un anagrama. Si compilas el programa y lo ejecutas con un diccionario ingl´es y el texto ((Universitat Jaume I)) descubrir´as algunos anagramas curiosos. Para ver qu´e hace exacta- mente, ejecuta $ anagrama </usr/dict/words -3 universitat jaume i) en el int´erprete de ´ordenes: por pantalla aparecer´an decenas de anagramas, entre ellos ((autism injure vitae)) y ((mutate via injuries)). Usando un diccionario espa˜nol y di- ferentes n´umeros de palabras obtendr´as, entre otros, ´estos: ((mutis, vieja uterina)) o ((mi jeta nueva, tu iris)). Ya sabes: puedes escribir programas ilegibles en C. ¡Procura que tus programas no merezcan una menci´on de honor en el concurso! Los lenguajes de programaci´on en los que el c´odigo no debe seguir un formato determinado de l´ıneas y/o bloques se denominan de formato libre. Python no es un lenguaje de formato libre; C s´ı. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 9 Este programa C incorrecto tiene varios errores que ya puedes detectar. Indica cu´ales son: 18 Introducci´on a la Programaci´on con C
  • 25. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C 1 #include <stdio.h> 2 3 int a, b; 4 5 scanf ("%d", &a); scanf ("%d", &b) 6 while (a <= b): 7 scanf ("%d", &a) 8 scanf ("%d", &b) 9 printf ("%d %dn", a, b); · 10 Indenta ((correctamente)) este programa C. 1 #include <stdio.h> 2 int main(void) 3 { 4 int a, b; 5 scanf ("%d", &a); 6 scanf ("%d", &b); 7 while(a > b) { 8 scanf ("%d", &a); 9 scanf ("%d", &b); 10 } 11 printf ("%d %dn", a, b); 12 return 0; 13 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.5. Hay dos tipos de comentario C99 permite escribir comentarios de dos formas distintas. Una es similar a la de Python: se marca el inicio de comentario con un s´ımbolo especial y ´este se prolonga hasta el final de l´ınea. La marca especial no es #, sino //. El segundo tipo de comentario puede ocupar m´as de una l´ınea: empieza con los caracteres /* y finaliza con la primera aparici´on del par de caracteres */. En este ejemplo aparecen comentarios que abarcan m´as de una l´ınea: maximo.c maximo.c 1 /********************************************************************* 2 * Un programa de ejemplo. 3 *-------------------------------------------------------------------- 4 * Prop´osito: mostrar algunos efectos que se pueden lograr con 5 * comentarios de C 6 *********************************************************************/ 8 #include <stdio.h> 9 10 /*--------------------------------------------------------------------- 11 * Programa principal 12 *-------------------------------------------------------------------*/ 14 15 int main(void) 16 { 17 int a, b, c; // Los tres n´umeros. 18 int m; // Variable para el m´aximo de los tres. 19 20 /* Lectura de un n´umero */ 21 printf ("a: "); scanf ("%d", &a); 22 /* ... de otro ... */ 23 printf ("b: "); scanf ("%d", &b); 24 /* ... y de otro m´as. */ 25 printf ("c: "); scanf ("%d", &c); 26 if (a > b) 27 if (a > c) //En este caso a > b y a > c. 28 m = a; Introducci´on a la Programaci´on con C 19
  • 26. 1.5 Hay dos tipos de comentario 29 else //Y en este otro caso b < a ≤ c. 30 m = c; 31 else 32 if (b > c) //En este caso a ≤ b y b > c. 33 m = b; 34 else //Y en este otro caso a ≤ b ≤ c. 35 m = c; 36 /* Impresi´on del resultado. */)) 37 printf ("El m´aximo de %d, %d y %d es %dn", a, b, c, m); 38 return 0; 39 } Uno de los comentarios empieza al principio de la l´ınea 1 y finaliza al final de la l´ıne 6 (sus dos ´ultimos caracteres visibles son un asterisco y una barra). Hay otro que empieza en la l´ınea 10 y finaliza en al l´ınea 12. Y hay otros que usan las marcas /* y */ en l´ıneas como la 20 o la 22, aunque hubi´esemos podidos usar en ambos casos la marca //. Los comentarios encerrados entre /* y */ no se pueden anidar. Este fragmento de programa es incorrecto: /* un /* comentario */ mal hecho */ ¿Por qu´e? Parece que hay un comentario dentro de otro, pero no es as´ı: el comentario que empieza en el primer par de caracteres /* acaba en el primer par de caracteres */, no en el segundo. El texto del ´unico comentario aparece aqu´ı enmarcado: /* un /* comentario */ mal hecho */ As´ı pues, el fragmento (( mal hecho */)) no forma parte de comentario alguno y no tiene sentido en C, por lo que el compilador detecta un error. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 11 Haciendo pruebas durante el desarrollo de un programa hemos decidido comentar una l´ınea del programa para que, de momento, no sea compilada. El programa nos queda as´ı: 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a, b, i, j; 6 7 scanf ("%d", &a); 8 scanf ("%d", &b); 9 i = a; 10 j = 1; 11 while (i <= b) { 12 /* printf ("%d %dn", i, j); */ 13 j *= 2; 14 i += 1; 15 } 16 printf ("%dn", j); 17 return 0; 18 } Compilamos el programa y el compilador no detecta error alguno. Ahora decidimos comentar el bucle while completo, as´ı que a˜nadimos un nuevo par de marcas de comentario (l´ıneas 11 y 17): 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a, b, i, j; 6 7 scanf ("%d", &a); 8 scanf ("%d", &b); 9 i = a; 20 Introducci´on a la Programaci´on con C
  • 27. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C 10 j = 1; 11 /* 12 while (i <= b) { 13 /* printf ("%d %dn", i, j); */ 14 j *= 2; 15 i += 1; 16 } 17 */ 19 printf ("%dn", j); 20 return 0; 21 } Al compilar nuevamente el programa aparecen mensajes de error. ¿Por qu´e? · 12 ¿Da problemas este otro programa con comentarios? 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a, b, i, j; 6 7 scanf ("%d", &a); 8 scanf ("%d", &b); 9 i = a; 10 j = 1; 11 /* 12 while (i <= b) { 13 // printf ("%d %dn", i, j); 14 j *= 2; 15 i += 1; 16 } 17 */ 19 printf ("%dn", j); 20 return 0; 21 } · 13 ¿C´omo se interpreta esta sentencia? 1 i = x //*y*/z++ 2 ; . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.6. Valores literales en C Por valores literales nos referimos a valores de n´umeros, cadenas y caracteres dados expl´ı- citamente. Afortunadamente, las reglas de escritura de literales en C son similares a las de Python. 1.6.1. Enteros Una forma natural de expresar un n´umero entero en C es mediante una secuencias de d´ıgitos. Por ejemplo, 45, 0 o 124653 son enteros. Al igual que en Python, est´a prohibido insertar espacios en blanco (o cualquier otro s´ımbolo) entre los d´ıgitos de un literal entero. Hay m´as formas de expresar enteros. En ciertas aplicaciones resulta ´util expresar un n´umero entero en base 8 (sistema octal) o en base 16 (sistema hexadecimal). Si una secuencia de d´ıgitos empieza en 0, se entiende que codifica un n´umero en base 8. Por ejemplo, 010 es el entero 8 (en base 10) y 0277 es el entero 191 (en base 10). Para codificar un n´umero en base 16 debes usar el par de caracteres 0x seguido del n´umero en cuesti´on. El literal 0xff, por ejemplo, codifica el valor decimal 255. Pero a´un hay una forma m´as de codificar un entero, una que puede resultar extra˜na al principio: mediante un car´acter entre comillas simples, que representa a su valor ASCII. El Introducci´on a la Programaci´on con C 21
  • 28. 1.6 Valores literales en C valor ASCII de la letra ((a min´uscula)), por ejemplo, es 97, as´ı que el literal ’a’ es el valor 97. Hasta tal punto es as´ı que podemos escribir expresiones como ’a’+1, que es el valor 98 o, lo que es lo mismo, ’b’. Se puede utilizar cualquiera de las secuencias de escape que podemos usar con las cadenas. El literal ’n’, por ejemplo, es el valor 10 (que es el c´odigo ASCII del salto de l´ınea). Ni ord ni chr En C no son necesarias las funciones ord o chr de Python, que convert´ıan caracteres en enteros y enteros en caracteres. Como en C los caracteres son enteros, no resulta necesario efectuar conversi´on alguna. 1.6.2. Flotantes Los n´umeros en coma flotante siguen la misma sintaxis que los flotantes de Python. Un n´umero flotante debe presentar parte decimal y/o exponente. Por ejemplo, 20.0 es un flotante porque tiene parte decimal (aunque sea nula) y 2e1 tambi´en lo es, pero porque tiene exponente (es decir, tiene una letra e seguida de un entero). Ambos representan al n´umero real 20.0. (Recuerda que 2e1 es 2 · 101 .) Es posible combinar en un n´umero parte decimal con exponente: 2.0e1 es un n´umero en coma flotante v´alido. 1.6.3. Cadenas As´ı como en Python puedes optar por encerrar una cadena entre comillas simples o dobles, en C s´olo puedes encerrarla entre comillas dobles. Dentro de las cadenas puedes utilizar secuencias de escape para representar caracteres especiales. Afortunadamente, las secuencias de escape son las mismas que estudiamos en Python. Por ejemplo, el salto de l´ınea es n y la comilla doble es "·. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 14 Traduce a cadenas C las siguientes cadenas Python: 1. "una cadena" 2. ’una cadena’ 3. "una "cadena"" 4. ’una "cadena"’ 5. ’una ’cadena’’ 6. "una cadena que ocupan dos l´ıneas" 7. "una cadena que no ocupa dos l´ıneas" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Te relacionamos las secuencias de escape que puedes necesitar m´as frecuentemente: 22 Introducci´on a la Programaci´on con C
  • 29. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C Secuencia Valor a (alerta): produce un aviso audible o visible. b (backspace, espacio atr´as): el cursor retrocede un espacio a la izquierda. f (form feed, alimentaci´on de p´agina): pasa a una nueva ((p´agina)). n (newline, nueva l´ınea): el cursor pasa a la primera posici´on de la siguiente l´ınea. r (carriage return, retorno de carro): el cursor pasa a la primera posici´on de la l´ınea actual. t (tabulador): desplaza el cursor a la siguiente marca de tabulaci´on. muestra la barra invertida. " muestra la comilla doble. n´umero octal muestra el car´acter cuyo c´odigo ASCII (o IsoLatin) es el n´umero octal indicado. El n´umero octal puede tener uno, dos o tres d´ıgitos octales. Por ejemplo "60" equivale a "0", pues el valor ASCII del car´acter cero es 48, que en octal es 60. xn´umero hexadecimal ´ıdem, pero el n´umero est´a codificado en base 16 y puede tener uno o dos d´ıgitos hexadecimales. Por ejemplo, "x30" tambi´en equivale a "0", pues 48 en decimal es 30 en hexadecimal. ? muestra el interrogante. Es pronto para aprender a utilizar variables de tipo cadena. Postergamos este asunto hasta el apartado 2.2. 1.7. C tiene un rico juego de tipos escalares En Python tenemos dos tipos num´ericos escalares: enteros y flotantes8 . En C hay una gran variedad de tipos escalares en funci´on del n´umero de cifras o de la precisi´on con la que deseamos trabajar, as´ı que nos permite tomar decisiones acerca del compromiso entre rango/precisi´on y ocupaci´on de memoria: a menor rango/precisi´on, menor ocupaci´on de memoria. No obstante, nosotros limitaremos nuestro estudio a cinco tipos de datos escalares: int, unsigned int, float, char y unsigned char. Puedes consultar el resto de tipos escalares en el ap´endice A. Encontrar´as una variedad enorme: enteros con diferente n´umero de bits, con y sin signo, flotantes de precisi´on normal y grande, booleanos, etc. Esa enorme variedad es uno de los puntos fuertes de C, pues permite ajustar el consumo de memoria a las necesidades de cada programa. En aras de la simplicidad expositiva, no obstante, no la consideraremos en el texto. 1.7.1. El tipo int El tipo de datos int se usar normalmente para representar n´umeros enteros. La especificaci´on de C no define el rango de valores que podemos representar con una variable de tipo int, es decir, no define el n´umero de bits que ocupa una variable de tipo int. No obstante, lo m´as frecuente es que ocupe 32 bits. Nosotros asumiremos en este texto que el tama˜no de un entero es de 32 bits, es decir, 4 bytes. Como los enteros se codifican en complemento a 2, el rango de valores que podemos repre- sentar es [−2147483648, 2147483647], es decir, [−231 , 231 − 1]. Este rango es suficiente para las aplicaciones que presentaremos. Si resulta insuficiente o excesivo para alguno de tus programas, consulta el cat´alogo de tipos que presentamos en el ap´endice A. En C, tradicionalmente, los valores enteros se han utilizado para codificar valores booleanos. El valor 0 representa el valor l´ogico ((falso)) y cualquier otro valor representa ((cierto)). En la ´ultima revisi´on de C se ha introducido un tipo booleano, aunque no lo usaremos en este texto porque, de momento, no es frecuente encontrar programas que lo usen. 1.7.2. El tipo unsigned int ¿Para qu´e desperdiciar el bit m´as significativo en una variable entera de 32 bits que nunca almacenar´a valores negativos? C te permite definir variables de tipo ((entero sin signo)). El tipo 8Bueno, esos son los que hemos estudiado. Python tiene, adem´as, enteros largos. Otro tipo num´erico no secuencial de Python es el complejo. Introducci´on a la Programaci´on con C 23
  • 30. 1.8 Se debe declarar el tipo de toda variable antes de usarla tiene un nombre compuesto por dos palabras: ((unsigned int)) (aunque la palabra unsigned, sin m´as, es sin´onimo de unsigned int). Gracias al aprovechamiento del bit extra es posible aumentar el rango de valores positivos representables, que pasa a ser [0, 232 − 1], o sea, [0, 4294967295]. 1.7.3. El tipo float El tipo de datos float representa n´umeros en coma flotante de 32 bits. La codificaci´on de coma flotante permite definir valores con decimales. El m´aximo valor que puedes almacenar en una variable de tipo float es 3.40282347 · 1038 . Recuerda que el factor exponencial se codifica en los programas C con la letra ((e)) (o ((E))) seguida del exponente. Ese valor, pues, se codifica as´ı en un programa C: 3.40282347e38. El n´umero no nulo m´as peque˜no (en valor absoluto) que puedes almacenar en una variable float es 1.17549435·10−38 (o sea, el literal flotante 1.17549435e-38). Da la impresi´on, pues, de que podemos representar n´umeros con 8 decimales. No es as´ı: la precisi´on no es la misma para todos los valores: es tanto mayor cuanto m´as pr´oximo a cero es el valor. 1.7.4. El tipo char El tipo char, aunque tenga un nombre que parezca sugerir el t´ermino ((car´acter)) (que en ingl´es es ((character))) designa en realidad a una variante de enteros: el conjunto de n´umeros que podemos representar (en complemento a 2) con un solo byte (8 bits). El rango de valores que puede tomar una variable de tipo char es muy limitado: [−128, 127]. Es frecuente usar variables de tipo char para almacenar caracteres (de ah´ı su nombre) codificados en ASCII o alguna de sus extensiones (como IsoLatin1). Si una variable a es de tipo char, la asignaci´on a=’0’ es absolutamente equivalente a la asignaci´on a=48, pues el valor ASCII del d´ıgito 0 es 48. 1.7.5. El tipo unsigned char Y del mismo modo que hab´ıa una versi´on para enteros de 32 bits sin signo, hay una versi´on de char sin signo: unsigned char. Con un unsigned char se puede representar cualquier entero en el rango [0, 255]. 1.8. Se debe declarar el tipo de toda variable antes de usarla Recuerda que en C toda variable usada en un programa debe declararse antes de ser usada. Declarar la variable consiste en darle un nombre (identificador) y asignarle un tipo. 1.8.1. Identificadores v´alidos Las reglas para construir identificadores v´alidos son las mismas que sigue Python: un identifi- cador es una sucesi´on de letras (del alfabeto ingl´es), d´ıgitos y/o el car´acter de subrayado ( ) cuyo primer car´acter no es un d´ıgito. Y al igual que en Python, no puedes usar una palabra reservada como identificador. He aqu´ı la relaci´on de palabras reservadas del lenguaje C: 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 y while 1.8.2. Sentencias de declaraci´on Una variable se declara precediendo su identificador con el tipo de datos de la variable. Este fragmento, por ejemplo, declara una variable de tipo entero, otra de tipo entero de un byte (o car´acter) y otra de tipo flotante: int a; char b; float c; 24 Introducci´on a la Programaci´on con C
  • 31. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C C, ocupaci´on de los datos, complemento a 2 y portabilidad Los n´umeros enteros con signo se codifican en complemento a 2. Con n bits puedes repre- sentar valores enteros en el rango [−2n−1 , 2n−1 − 1]. Los valores positivos se representan en binario, sin m´as. Los valores negativos se codifican representando en binario su valor absoluto, invirtiendo todos sus bits y a˜nadiendo 1 al resultado. Sup´on que trabajamos con datos de tipo char (8 bits). El valor 28 se representa en binario as´ı 00011100. El valor −28 se obtiene tomando la representaci´on binaria de 28, invirtiendo sus bits (11100011), y a˜nadiendo uno. El resultado es 11100100. Una ventaja de la notaci´on en complemento a 2 es que simplifica el dise˜no de circuitos para la realizaci´on de c´alculos aritm´eticos. Por ejemplo, la resta es una simple suma. Si deseas restar a 30 el valor 28, basta con sumar 30 y -28 con la misma circuiter´ıa electr´onica utilizada para efectuar sumas convencionales: 00011110 + 11100100 00000010 El complemento a 2 puede gastarte malas pasadas si no eres consciente de c´omo fun- ciona. Por ejemplo, sumar dos n´umeros positivos puede producir un resultado ¡negativo! Si trabajas con 8 bits y sumas 127 y 1, obtienes el valor −128: 01111111 + 00000001 10000000 Este fen´omeno se conoce como ((desbordamiento)). C no aborta la ejecuci´on del programa cuando se produce un desbordamiento: da por bueno el resultado y sigue. Mala cosa: puede que demos por bueno un programa que est´a produciendo resultados err´oneos. El est´andar de C no define de modo claro la ocupaci´on de cada uno de sus tipos de datos lo cual, unido a fen´omenos de desbordamiento, dificulta notablemente la portabilidad de los programas. En la mayor´ıa de los compiladores y ordenadores actuales, una variable de tipo int ocupa 32 bits. Sin embargo, en ordenadores m´as antiguos era frecuente que ocupara s´olo 16. Un programa que suponga una representaci´on mayor que la real puede resultar en la comisi´on de errores en tiempo de ejecuci´on. Por ejemplo, si una variable a de tipo int ocupa 32 bits y vale 32767, ejecutar la asignaci´on a = a + 1 almacenar´a en a el valor 32768; pero si el tipo int ocupa 16 bits, se almacena el valor −32768. Puede que demos por bueno un programa al compilarlo y ejecutarlo en una plataforma determinada, pero que falle estrepitosamente cuando lo compilamos y ejecutamos en una plataforma diferente. O, peor a´un, puede que el error pase inadvertido durante mucho tiem- po: el programa no abortar´a la ejecuci´on y producir´a resultados incorrectos que podemos no detectar. Es un problema muy grave. Los problemas relacionados con la garant´ıa de poder ejecutar un mismo programa en diferentes plataformas se conocen como problemas de portabilidad. Pese a los muchos pro- blemas de portabilidad de C, es el lenguaje de programaci´on en el que se ha escrito buena parte de los programas que hoy ejecutamos en una gran variedad de plataformas. char, unsigned char, ASCII e IsoLatin1 La tabla ASCII tiene caracteres asociados a valores comprendidos entre 0 y 127, as´ı que todo car´acter ASCII puede almacenarse en una variable de tipo char. Pero, en realidad, nosotros no usamos la tabla ASCII ((pura)), sino una extensi´on suya: IsoLatin1 (tambi´en conocida por ISO-8859-1 o ISO-8859-15, si incluye el s´ımbolo del euro). La tabla IsoLatin1 nos permite utilizar caracteres acentuados y otros s´ımbolos especiales propios de las lenguas rom´anicas occidentales. ¿Qu´e ocurre si asignamos a una variable de tipo char el car´acter ’´a’? El c´odigo IsoLatin1 de ’´a’ es 225, que es un valor num´erico mayor que 127, el m´aximo valor entero que podemos almacenar en una variable de tipo char. Mmmm. S´ı, pero 225 se codifica en binario como esta secuencia de ceros y unos: 11100001. Si interpretamos dicha secuencia en complemento a dos, tenemos el valor −31, y ese es, precisamente, el valor que resulta almacenado. Podemos evitar este inconveniente usando el tipo unsigned char, pues permite almacenar valores entre 0 y 255. Introducci´on a la Programaci´on con C 25
  • 32. 1.8 Se debe declarar el tipo de toda variable antes de usarla Se puede declarar una serie de variables del mismo tipo en una sola sentencia de declaraci´on separando sus identificadores con comas. Este fragmento, por ejemplo, declara tres variables de tipo entero y otras dos de tipo flotante. int x, y, z; float u, v; En sumatorio.c se declaran tres variables de tipo int, a, b y c, y una de tipo float, s. Una variable declarada como de tipo entero s´olo puede almacenar valores de tipo entero. Una vez se ha declarado una variable, es imposible cambiar su tipo, ni siquiera volviendo a declararla. Este programa, por ejemplo, es incorrecto por el intento de redeclarar el tipo de la variable a: redeclara.c E redeclara.c E 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a; 6 float a; 7 8 a = 2; 9 return 0; 10 } Al compilarlo obtenemos este mensaje de error: $ gcc redeclara.c -o redeclara redeclara.c: In function ‘main’: redeclara.c:6: conflicting types for ‘a’ redeclara.c:5: previous declaration of ‘a’ El compilador nos indica que la variable a presenta un conflicto de tipos en la l´ınea 6 y que ya hab´ıa sido declarada previamente en la l´ınea 5. 1.8.3. Declaraci´on con inicializaci´on Debes tener presente que el valor inicial de una variable declarada est´a indefinido. Jam´as debes acceder al contenido de una variable que no haya sido previamente inicializada. Si lo haces, el compilador no detectar´a error alguno, pero tu programa presentar´a un comportamiento indeterminado: a veces funcionar´a bien, y a veces mal, lo cual es peor que un funcionamiento siempre incorrecto, pues podr´ıas llegar a dar por bueno un programa mal escrito. En esto C se diferencia de Python: Python abortaba la ejecuci´on de un programa cuando se intentaba usar una variable no inicializada; C no aborta la ejecuci´on, pero presenta un comportamiento indeterminado. Puedes inicializar las variables en el momento de su declaraci´on. Para ello, basta con a˜nadir el operador de asignaci´on y un valor a continuaci´on de la variable en cuesti´on. Mira este ejemplo: 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a = 2; 6 float b = 2.0, c, d = 1.0, e; 7 8 return 0; 9 } En ´el, las variables a, b y d se inicializan en la declaraci´on y las variables c y e no tienen valor definido al ser declaradas. Recuerda que acceder a variables no inicializadas es una fuente de graves errores. Acost´umbrate a inicializar las variables tan pronto puedas. 26 Introducci´on a la Programaci´on con C
  • 33. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C 1.9. Salida por pantalla La funci´on de impresi´on de informaci´on en pantalla utilizada habitualmente es printf . Es una funci´on disponible al incluir stdio.h en el programa. El uso de printf es ligeramente m´as com- plicado que el de la sentencia print de Python, aunque no te resultar´a dif´ıcil si ya has aprendido a utilizar el operador de formato en Python (%). En su forma de uso m´as simple, printf permite mostrar una cadena por pantalla. 1 #include <stdio.h> 2 3 int main(void) 4 { 5 printf ("Una cadena"); 6 printf ("y otra."); 7 return 0; 8 } La funci´on printf no a˜nade un salto de l´ınea autom´aticamente, como s´ı hac´ıa print en Python. En el programa anterior, ambas cadenas se muestran una a continuaci´on de otra. Si deseas que haya un salto de l´ınea, deber´as escribir n al final de la cadena. 1 #include <stdio.h> 2 3 int main(void) 4 { 5 printf ("Una cadenan"); 6 printf ("y otra.n"); 7 return 0; 8 } 1.9.1. Marcas de formato para la impresi´on de valores con printf Marcas de formato para n´umeros Para mostrar n´umeros enteros o flotantes has de usar necesariamente cadenas con formato. Afortunadamente, las marcas que aprendiste al estudiar Python se utilizan en C. Eso s´ı, hay algunas que no te hemos presentado a´un y que tambi´en se recogen en esta tabla: Tipo Marca int %d unsigned int %u float %f char %hhd unsigned char %hhu Por ejemplo, si a es una variable de tipo int con valor 5, b es una variable de tipo float con valor 1.0, y c es una variable de tipo char con valor 100, esta llamada a la funci´on printf : printf ("Un entero: %d, un flotante: %f, un byte: %hhdn", a, b, c); muestra por pantalla esto: Un entero: 5, un flotante: 1.000000, un byte: 100 ¡Ojo! a la cadena de formato le sigue una coma, y no un operador de formato como suced´ıa en Python. Cada variable se separa de las otras con una coma. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 15 ¿Que mostrar´a por pantalla esta llamada a printf suponiendo que a es de tipo entero y vale 10? printf ("%d-%dn", a+1, 2+2); . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Introducci´on a la Programaci´on con C 27
  • 34. 1.9 Salida por pantalla Las marcas de formato para enteros aceptan modificadores, es decir, puedes alterar la repre- sentaci´on introduciendo ciertos caracteres entre el s´ımbolo de porcentaje y el resto de la marca. Aqu´ı tienes los principales: Un n´umero positivo: reserva un n´umero de espacios determinado (el que se indique) para representar el valor y muestra el entero alineado a la derecha. Ejemplo: la sentencia printf ("[%6d]", 10); muestra en pantalla: [ 10] Un n´umero negativo: reserva tantos espacios como indique el valor absoluto del n´umero para representar el entero y muestra el valor alineado a la izquierda. Ejemplo: la sentencia printf ("[%-6d]", 10); muestra en pantalla: [10 ] Un n´umero que empieza por cero: reserva tantos espacios como indique el n´umero para representar el entero y muestra el valor alineado a la derecha. Los espacios que no ocupa el entero se rellenan con ceros. Ejemplo: la sentencia printf ("[%06d]", 10); muestra en pantalla: [000010] El signo +: muestra expl´ıcitamente el signo (positivo o negativo) del entero. Ejemplo: la sentencia printf ("[%+6d]", 10); muestra en pantalla: [ +10] Hay dos notaciones alternativas para la representaci´on de flotantes que podemos seleccionar mediante la marca de formato adecuada: Tipo Notaci´on Marca float Convencional %f float Cient´ıfica %e La forma convencional muestra los n´umeros con una parte entera y una decimal separadas por un punto. La notaci´on cient´ıfica representa al n´umero como una cantidad con una sola cifra entera y una parte decimal, pero seguida de la letra ((e)) y un valor entero. Por ejemplo, en notaci´on cient´ıfica, el n´umero 10.1 se representa con 1.010000e+01 y se interpreta as´ı: 1.01×101 . Tambi´en puedes usar modificadores para controlar la representaci´on en pantalla de los flotan- tes. Los modificadores que hemos presentado para los enteros son v´alidos aqu´ı. Tienes, adem´as, la posibilidad de fijar la precisi´on: Un punto seguido de un n´umero: indica cu´antos decimales se mostrar´an. Ejemplo: la sentencia printf ("[%6.2f]", 10.1); muestra en pantalla: [ 10.10] 28 Introducci´on a la Programaci´on con C
  • 35. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C Marcas de formato para texto Y a´un nos queda presentar las marcas de formato para texto. C distingue entre caracteres y cadenas: Tipo Marca car´acter %c cadena %s ¡Atenci´on! La marca %c muestra como car´acter un n´umero entero. Naturalmente, el car´acter que se muestra es el que corresponde al valor entero seg´un la tabla ASCII (o, en tu ordenador, IsoLatin1 si el n´umero es mayor que 127). Por ejemplo, la sentencia printf ("[%c]", 97); muestra en pantalla: [a] Recuerda que el valor 97 tambi´en puede representarse con el literal ’a’, as´ı que esta otra sentencia printf ("[%c]", ’a’); tambi´en muestra en pantalla esto: [a] A´un no sabemos almacenar cadenas en variables, as´ı que poca aplicaci´on podemos encontrar de momento a la marca %s. He aqu´ı, de todos modos, un ejemplo trivial de uso: printf ("[%s]", "una cadena"); En pantalla se muestra esto: [una cadena] Tambi´en puedes usar n´umeros positivos y negativos como modificadores de estas marcas. Su efecto es reservar los espacios que indiques y alinear a derecha o izquierda. Aqu´ı tienes un programa de ejemplo en el que se utilizan diferentes marcas de formato con y sin modificadores. modificadores.c modificadores.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 char c = ’a’; 6 int i = 1000000; 7 float f = 2e1; 8 9 printf ("c : %c %hhd <- ! IMPORTANTE! Estudia la diferencia.n", c, c); 10 printf ("i : %d |%10d|%-10d|n", i, i, i); 11 printf ("f : %f |%10.2f|%+4.2f|n", f, f, f); 12 return 0; 13 } El resultado de ejecutar el programa es la impresi´on por pantalla del siguiente texto: c : a 97 <- ! IMPORTANTE! Estudia la diferencia. i : 1000000 | 1000000|1000000 | f : 20.000000 | 20.00|+20.00| . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 16 ¿Qu´e muestra por pantalla cada uno de estos programas? Introducci´on a la Programaci´on con C 29
  • 36. 1.9 Salida por pantalla a) ascii1.c ascii1.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 char i; 6 for (i=’A’; i<=’Z’; i++) 7 printf ("%c", i); 8 printf ("n"); 9 return 0; 10 } b) ascii2.c ascii2.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 char i; 6 for (i=65; i<=90; i++) 7 printf ("%c", i); 8 printf ("n"); 9 return 0; 10 } c) ascii3.c ascii3.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int i; 6 for (i=’A’; i<=’Z’; i++) 7 printf ("%d ", i); 8 printf ("n"); 9 return 0; 10 } d) ascii4.c ascii4.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int i; 6 for (i=’A’; i<=’Z’; i++) 7 printf ("%d-%c ", i, i); 8 printf ("n"); 9 return 0; 10 } e) ascii5.c ascii5.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 char i; 6 for (i=’A’; i<=’z’; i++) // Ojo: la z es min´uscula. 7 printf ("%d ", (int) i); 8 printf ("n"); 9 return 0; 10 } · 17 Dise˜na un programa que muestre la tabla ASCII desde su elemento de c´odigo num´erico 32 hasta el de c´odigo num´erico 126. En la tabla se mostrar´an los c´odigos ASCII, adem´as de las respectivas representaciones como caracteres de sus elementos. Aqu´ı tienes las primeras y 30 Introducci´on a la Programaci´on con C
  • 37. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C ´ultimas l´ıneas de la tabla que debes mostrar (debes hacer que tu programa muestre la informa- ci´on exactamente como se muestra aqu´ı): +---------+----------+ | Decimal | Car´acter | +---------+----------+ | 32 | | | 33 | ! | | 34 | " | | 35 | # | | 36 | $ | | 37 | % | ... ... | 124 | | | | 125 | } | | 126 | ~ | +---------+----------+ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Hay un rico juego de marcas de formato y las recogemos en el ap´endice A. Cons´ultalo si usas tipos diferentes de los que presentamos en el texto o si quieres mostrar valores enteros en base 8 o 16. En cualquier caso, es probable que necesites conocer una marca especial, %%, que sirve para mostrar el s´ımbolo de porcentaje. Por ejemplo, la sentencia printf ("[%d%%]", 100); muestra en pantalla: [100%] 1.10. Variables y direcciones de memoria Antes de presentar con cierto detalle la entrada de datos por teclado mediante scanf , nos conviene detenernos brevemente para estudiar algunas cuestiones relativas a las variables y la memoria que ocupan. Recuerda que la memoria es una sucesi´on de celdas numeradas y que una direcci´on de memoria no es m´as que un n´umero entero. La declaraci´on de una variable supone la reserva de una zona de memoria lo suficientemente grande para albergar su contenido. Cuando declaramos una variable de tipo int, por ejemplo, se reservan 4 bytes de memoria en los que se almacenar´a (codificado en complemento a 2) el valor de dicha variable. Modificar el valor de la variable mediante una asignaci´on supone modificar el patr´on de 32 bits (4 bytes) que hay en esa zona de memoria. Este programa, por ejemplo, 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a, b; 6 7 a = 0; 8 b = a + 8; 9 10 return 0; 11 } reserva 8 bytes para albergar dos valores enteros.9 Imagina que a ocupa los bytes 1000–1003 y b ocupa los bytes 1004–1007. Podemos representar la memoria as´ı: 9En el apartado 3.5.2 veremos que la reserva se produce en una zona de memoria especial llamada pila. No conviene que nos detengamos ahora a considerar los matices que ello introduce en el discurso. Introducci´on a la Programaci´on con C 31
  • 38. 1.10 Variables y direcciones de memoria 996: 1000: 1004: 1008: a b 01010010 10101000 01110011 11110010 01011010 00111101 00111010 11010111 10111011 10010110 01010010 01010011 11010111 01000110 11110010 01011101 Observa que, inicialmente, cuando se reserva la memoria, ´esta contiene un patr´on de bits arbitrario. La sentencia a = 0 se interpreta como ((almacena el valor 0 en la direcci´on de memoria de a)), es decir, ((almacena el valor 0 en la direcci´on de memoria 1000))10 . Este es el resultado de ejecutar esa sentencia: 996: 1000: 1004: 1008: a b 01010010 10101000 01110011 11110010 00000000 00000000 00000000 00000000 10111011 10010110 01010010 01010011 11010111 01000110 11110010 01011101 La asignaci´on b = a + 8 se interpreta como ((calcula el valor que resulta de sumar 8 al contenido de la direcci´on de memoria 1000 y deja el resultado en la direcci´on de memoria 1004)). 996: 1000: 1004: 1008: a b 01010010 10101000 01110011 11110010 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001000 11010111 01000110 11110010 01011101 Hemos supuesto que a est´a en la direcci´on 1000 y b en la 1004, pero ¿podemos saber en qu´e direcciones de memoria se almacenan realmente a y b? S´ı: el operador & permite conocer la direcci´on de memoria en la que se almacena una variable: direcciones.c direcciones.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a, b; 6 7 a = 0; 8 b = a + 8; 9 10 printf ("Direcci´on de a: %un", (unsigned int)&a); 11 printf ("Direcci´on de b: %un", (unsigned int)&b); 12 13 return 0; 14 } Observa que usamos la marca de formato %u para mostrar el valor de la direcci´on de memoria, pues debe mostrarse como entero sin signo. La conversi´on a tipo unsigned int evita molestos mensajes de aviso al compilar.11 Al ejecutar el programa tenemos en pantalla el siguiente texto (puede que si ejecutas t´u mismo el programa obtengas un resultado diferente): 10En realidad, en la zona de memoria 1000–1003, pues se modifica el contenido de 4 bytes. En aras de la brevedad, nos referiremos a los 4 bytes s´olo con la direcci´on del primero de ellos. 11Hay un marca especial, %p, que muestra directamente la direcci´on de memoria sin necesidad de efectuar la conversi´on a unsigned int, pero lo hace usando notaci´on hexadecimal. 32 Introducci´on a la Programaci´on con C
  • 39. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C Direcci´on de a: 3221222580 Direcci´on de b: 3221222576 O sea, que en realidad este otro gr´afico representa mejor la disposici´on de las variables en memoria: 3221222572: 3221222576: 3221222580: 3221222584: b a 01010010 10101000 01110011 11110010 00000000 00000000 00000000 00001000 00000000 00000000 00000000 00000000 11010111 01000110 11110010 01011101 Normalmente no necesitamos saber en qu´e direcci´on de memoria se almacena una variable, as´ı que no recurriremos a representaciones gr´aficas tan detalladas como las que hemos presen- tado. Usualmente nos conformaremos con representar las variables escalares mediante cajas y representaremos su valor de una forma m´as c´omodamente legible que como una secuencia de bits. La representaci´on anterior se simplificar´a, pues, as´ı: 0a 8b Las direcciones de memoria de las variables se representar´an con flechas que apuntan a sus correspondientes cajas: &a 0a &b 8b Ahora que hemos averiguado nuevas cosas acerca de las variables, vale la pena que reflexio- nemos brevemente sobre el significado de los identificadores de variables all´ı donde aparecen. Considera este sencillo programa: 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a, b; 6 7 a = 0; 8 b = a; 9 scanf ("%d", &b); 10 a = a + b; 11 12 return 0; 13 } ¿C´omo se interpreta la sentencia de asignaci´on a = 0? Se interpreta como ((almacena el valor 0 en la direcci´on de memoria de a)). ¿Y b = a?, ¿c´omo se interpreta? Como ((almacena una copia del contenido de a en la direcci´on de memoria de b)). F´ıjate bien, el identificador a recibe interpretaciones diferentes seg´un aparezca a la izquierda o a la derecha de una asignaci´on: a la izquierda del igual, significa ((la direcci´on de a)), y a la derecha, es decir, en una expresi´on, significa ((el contenido de a)). La funci´on scanf necesita una direcci´on de memoria para saber d´onde debe depositar un resultado. Como no estamos en una sentencia de asignaci´on, sino en una expresi´on, es necesario Introducci´on a la Programaci´on con C 33
  • 40. 1.11 Entrada por teclado que obtengamos expl´ıcitamente la direcci´on de memoria con el operador &b. As´ı, para leer por teclado el valor de b usamos la llamada scanf ("%d", &b). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 18 Interpreta el significado de la sentencia a = a + b. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.11. Entrada por teclado La funci´on scanf , disponible al incluir stdio.h, permite leer datos por teclado. La funci´on scanf se usa de un modo similar a printf : su primer argumento es una cadena con marcas de formato. A ´este le siguen una o m´as direcciones de memoria. Si deseas leer por teclado el valor de una variable entera a, puedes hacerlo as´ı: scanf ("%d", &a); Observa que la variable cuyo valor se lee por teclado va obligatoriamente precedida por el operador &: es as´ı como obtenemos la direcci´on de memoria en la que se almacena el valor de la variable. Uno de los errores que cometer´as con mayor frecuencia es omitir el car´acter & que debe preceder a todas las variables escalares en scanf . Recuerda: la funci´on scanf recibe estos datos: Una cadena cuya marca de formato indica de qu´e tipo es el valor que vamos a leer por teclado: Tipo Marca int %d unsigned int %u float %f char como entero %hhd char como car´acter %c unsigned char como entero %hhu unsigned char como car´acter %c La direcci´on de memoria que corresponde al lugar en el que se depositar´a el valor le´ıdo. Debemos proporcionar una direcci´on de memoria por cada marca de formato indicada en el primero argumento. Observa que hay dos formas de leer un dato de tipo char o unsigned char: como entero (de un byte con o sin signo, respectivamente) o como car´acter. En el segundo caso, se espera que el usuario teclee un solo car´acter y se almacenar´a en la variable su valor num´erico seg´un la tabla ASCII o su extensi´on IsoLatin. Una advertencia: la lectura de teclado en C presenta numerosas dificultades pr´acticas. Es muy recomendable que leas el ap´endice B antes de seguir estudiando y absolutamente necesario que lo leas antes de empezar a practicar con el ordenador. Si no lo haces, muchos de tus programas presentar´an un comportamiento muy extra˜no y no entender´as por qu´e. T´u mismo. 1.12. Expresiones Muchos de los s´ımbolos que representan a los operadores de Python que ya conoces son los mis- mos en C. Los presentamos ahora agrupados por familias. (Consulta los niveles de precedencia y asociatividad en la tabla de la p´agina 40.) Presta especial atenci´on a los operadores que no conoces por el lenguaje de programaci´on Python, como son los operadores de bits, el operador condicional o los de incremento/decremento. Operadores aritm´eticos Suma (+), resta (-), producto (*), divisi´on (/), m´odulo o resto de la divisi´on (%), identidad (+ unario), cambio de signo (- unario). No hay operador de exponenciaci´on.12 12Pero hay una funci´on de la biblioteca matem´atica que permite calcular la potencia de un n´umero: pow. 34 Introducci´on a la Programaci´on con C
  • 41. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C Errores frecuentes en el uso de scanf Es responsabilidad del programador pasar correctamente los datos a scanf . Un error que puede tener graves consecuencias consiste en pasar incorrectamente la direcci´on de memoria en la que dejar´a el valor le´ıdo. Este programa, por ejemplo, es err´oneo: scanf ("%d", a); La funci´on scanf no est´a recibiendo la direcci´on de memoria en la que ((reside)) a, sino el valor almacenado en a. Si scanf interpreta dicho valor como una direcci´on de memoria (cosa que hace), guardar´a en ella el n´umero que lea de teclado. ¡Y el compilador no necesariamente detectar´a el error! El resultado es catastr´ofico. Otro error t´ıpico al usar scanf consiste en confundir el tipo de una variable y/o la marca de formato que le corresponde. Por ejemplo, imagina que c es una variable de tipo char. Este intento de lectura de su valor por teclado es err´oneo: scanf ("%d", &c); A scanf le estamos pasando la direcci´on de memoria de la variable c. Hasta ah´ı, bien. Pero c s´olo ocupa un byte y a scanf le estamos diciendo que ((rellene)) 4 bytes con un n´umero entero a partir de esa direcci´on de memoria. Otro error de consecuencias grav´ısimas. La marca de formato adecuada para leer un n´umero de tipo char hubiera sido %hhd. scanf ("%hhd", &c); La divisi´on de dos n´umeros enteros proporciona un resultado de tipo entero (como ocurr´ıa en Python). Los operadores aritm´eticos s´olo funcionan con datos num´ericos13 . No es posible, por ejem- plo, concatenar cadenas con el operador + (cosa que s´ı pod´ıamos hacer en Python). La dualidad car´acter-entero del tipo char hace que puedas utilizar la suma o la resta (o cualquier otro operador aritm´etico) con variables o valores de tipo char. Por ejemplo ’a’ + 1 es una expresi´on v´alida y su valor es ’b’ (o, equivalentemente, el valor 98, ya que ’a’ equivale a 97). (Recuerda, no obstante, que un car´acter no es una cadena en C, as´ı que "a" + 1 no es "b".) Operadores l´ogicos Negaci´on o no-l´ogica (!), y-l´ogica o conjunci´on (&&) y o-l´ogica o disyun- ci´on (||). Los s´ımbolos son diferentes de los que aprendimos en Python. La negaci´on era all´ı not, la conjunci´on era and y la disyunci´on or. C sigue el convenio de que 0 significa falso y cualquier otro valor significa cierto. As´ı pues, cualquier valor entero puede interpretarse como un valor l´ogico, igual que en Python. Operadores de comparaci´on Igual que (==), distinto de (!=), menor que (<), mayor que (>), menor o igual que (<=), mayor o igual que (>=). Son viejos conocidos. Una diferencia con respecto a Python: s´olo puedes usarlos para comparar valores escalares. No puedes, por ejemplo, comparar cadenas mediante estos operadores. La evaluaci´on de una comparaci´on proporciona un valor entero: 0 si el resultado es falso y cualquier otro si el resultado es cierto (aunque normalmente el valor para cierto es 1). Operadores de bits Complemento (~), ((y)) (&), ((o)) (|), ((o)) exclusiva (^), desplazamiento a izquierdas (<<), desplazamiento a derechas (>>). Estos operadores trabajan directamente con los bits que codifican un valor entero. Aunque tambi´en est´an disponibles en Python, no los estudiamos entonces porque son de uso infrecuente en ese lenguaje de programaci´on. 13Y la suma y la resta trabajan tambi´en con punteros. Ya estudiaremos la denominada ((aritm´etica de punteros)) m´as adelante. Introducci´on a la Programaci´on con C 35
  • 42. 1.12 Expresiones -Wall Cuando escribimos un texto en castellano podemos cometer tres tipos de errores: Errores l´exicos: escribimos palabras incorrectamente, con errores ortogr´aficos, o usa- mos palabras inexistentes. Por ejemplo: ((herror)), ((l´ecsico)), ((jerig´ondor)). Errores sint´acticos: aunque las palabras son v´alidas y est´an correctamente escritas, faltan componentes de una frase (como el sujeto o el verbo), no hay concordancia entre componentes de la frase, los componentes de la frase no ocupan la posici´on ade- cuada, etc. Por ejemplo: ((el error sint´actica son)), ((la compilador detect´o errores)). Errores sem´anticos: la frase est´a correctamente construida pero carece de significado v´alido en el lenguaje. Por ejemplo: ((el compilador silb´o una tonada en v´ıdeo)), ((los osos son enteros con decimales romos)). Lo mismo ocurre con los programas C; pueden contener errores de los tres tipos: Errores l´exicos: usamos car´acteres no v´alidos o construimos incorrectamente compo- nentes elementales del programa (como identificadores, cadenas, palabras clave, etc.). Por ejemplo: ((@3)), (("una cadena sin cerrar)). Errores sint´acticos: construimos mal una sentencia aunque usamos palabras v´alidas. Por ejemplo: ((while a < 10 { a += 1; })), ((b = 2 * / 3;)). Errores sem´anticos: la sentencia no tiene un significado ((v´alido)). Por ejemplo, si a es de tipo float, estas sentencias contienen errores sem´anticos: ((scanf ("%d", &a);)) (se trata de leer el valor de a como si fuera un entero), ((if (a = 1.0) { a = 2.0; })) (no se est´a comparando el valor de a con 1.0, sino que se asigna el valor 1.0 a a). El compilador de C no deja pasar un solo error l´exico o sint´actico: cuando lo detecta, nos informa del error y no genera traducci´on a c´odigo de m´aquina del programa. Con los errores sem´anticos, sin embargo, el compilador es m´as indulgente: la filosof´ıa de C es suponer que el programador puede tener una buena raz´on para hacer algunas de las cosas que expresa en los programas, aunque no siempre tenga un significado ((correcto)) a primera vista. No obstante, y para seg´un qu´e posibles errores, el compilador puede emitir avisos (warnings). Es posible regular hasta qu´e punto deseamos que el compilador nos proporcione avisos. La opci´on -Wall (((Warning all)), que significa ((todos los avisos))) activa la detecci´on de posibles errores sem´anticos, notific´andolos como avisos. Este programa err´oneo, por ejemplo, no genera ning´un aviso al compilarse sin -Wall: semanticos.c E semanticos.c E 1 #include <stdio.h> 2 int main(void) 3 { 4 float a; 5 scanf ("%d", &a); 6 if (a = 0.0) { a = 2.0; } 7 return 0; 8 } Pero si lo compilas con ((gcc -Wall semanticos.c -o semanticos)), aparecen avisos (warnings) en pantalla: $ gcc -Wall semanticos.c -o semanticos semanticos.c: In function ‘main’: semanticos.c:5: warning: int format, float arg (arg 2) semanticos.c:6: warning: suggest parentheses around assignment used as truth value El compilador advierte de errores sem´anticos en las l´ıneas 5 y 6. Te har´a falta bastante pr´actica para aprender a descifrar mensajes tan parcos o extra˜nos como los que produce gcc, as´ı que conviene que te acostumbres a compilar con -Wall. (Y hazlo siempre que tu programa presente un comportamiento an´omalo y no hayas detectado errores l´exicos o sint´acticos.) El operador de complemento es unario e invierte todos los bits del valor. Tanto & como | 36 Introducci´on a la Programaci´on con C
  • 43. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C Lecturas m´ultiples con scanf No te hemos contado todo sobre scanf . Puedes usar scanf para leer m´as de un valor. Por ejemplo, este programa lee dos valores enteros con un solo scanf : lectura multiple.c lectura multiple.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a, b; 6 printf ("Introduce dos enteros: "); 7 scanf ("%d %d", &a, &b); 8 printf ("Valores le´ıdos: %d y %dn", a, b); 9 return 0; 10 } Tambi´en podemos especificar con cierto detalle c´omo esperamos que el usuario introduzca la informaci´on. Por ejemplo, con scanf ("%d-%d", &a, &b) indicamos que el usuario de- be separar los enteros con un gui´on; y con scanf ("(%d,%d)", &a, &b) especificamos que esperamos encontrar los enteros encerrados entre par´entesis y separados por comas. Lee la p´agina de manual de scanf (escribiendo man 3 scanf en el int´erprete de ´ordenes Unix) para obtener m´as informaci´on. y ^ son operadores binarios. El operador & devuelve un valor cuyo n-´esimo bit es 1 si y s´olo si los dos bits de la n-´esima posici´on de los operandos son tambi´en 1. El operador | devuelve 0 en un bit si y solo si los correspondientes bits en los operandos son tambi´en 0. El operador ^ devuelve 1 si y s´olo si los correspondientes bits en los operandos son diferentes. Lo entender´as mejor con un ejemplo. Imagina que a y b son variables de tipo char que valen 6 y 3, respectivamente. En binario, el valor de a se codifica como 00000110 y el valor de b como 00000011. El resultado de a | b es 7, que corresponde al valor en base diez del n´umero binario 000000111. El resultado de a & b es, en binario, 000000010, es decir, el valor decimal 2. El resultado binario de a ^ b es 000000101, que en base 10 es 5. Finalmente, el resultado de ~a es 11111001, es decir, −7 (recuerda que un n´umero con signo est´a codificado en complemento a 2, as´ı que si su primer bit es 1, el n´umero es negativo). Los operadores de desplazamiento desplazan los bits un n´umero dado de posiciones a izquierda o derecha. Por ejemplo, 16 como valor de tipo char es 00010000, as´ı que 16 << 1 es 32, que en binario es 00100000, y 16 >> 1 es 8, que en binario es 00001000. Operadores de bits y programaci´on de sistemas C presenta una enorme colecci´on de operadores, pero quiz´a los que te resulten m´as llamativos sean los operadores de bits. Dif´ıcilmente los utilizar´as en programas convencionales, pero son insustituibles en la programaci´on de sistemas. Cuando manejes informaci´on a muy bajo nivel es probable que necesites acceder a bits y modificar sus valores. Por ejemplo, el control de ciertos puertos del ordenador pasa por leer y asignar valores concretos a ciertos bits de direcciones virtuales de memoria. Puede que poner a 1 el bit menos significativo de determinada direcci´on permita detener la actividad de una impresora conectada a un puerto paralelo, o que el bit m´as significativo nos alerte de si falta papel en la impresora. Si deseas saber si un bit est´a o no activo, puedes utilizar los operadores & y <<. Para saber, por ejemplo, si el octavo bit de una variable x est´a activo, puedes calcular x & (1 << 7). Si el resultado es cero, el bit no est´a activo; en caso contrario, est´a activo. Para fijar a 1 el valor de ese mismo bit, puedes hacer x = x | (1 << 7). Los operadores de bits emulan el comportamiento de ciertas instrucciones disponibles en los lenguajes ensambladores. La facilidad que proporciona C para escribir programas de ((bajo nivel)) es grande, y por ello C se considera el lenguaje a elegir cuando hemos de escribir un controlador para un dispositivo o el c´odigo de un sistema operativo. Introducci´on a la Programaci´on con C 37
  • 44. 1.12 Expresiones Operadores de asignaci´on Asignaci´on (=), asignaci´on con suma (+=), asignaci´on con resta (-=), asignaci´on con producto (*=), asignaci´on con divisi´on (/=), asignaci´on con m´odulo (%=), asignaci´on con desplazamiento a izquierda (<<=), asignaci´on con desplazamiento a derecha (>>=), asignaci´on con ((y)) (&=), asignaci´on con ((o)) (|=), asignaci´on con ((o)) exclusiva (^=). Puede resultarte extra˜no que la asignaci´on se considere tambi´en un operador. Que sea un operador permite escribir asignaciones m´ultiples como ´esta: a = b = 1; Es un operador asociativo por la derecha, as´ı que las asignaciones se ejecutan en este orden: a = (b = 1); El valor que resulta de evaluar una asignaci´on con = es el valor asignado a su parte izquierda. Cuando se ejecuta b = 1, el valor asignado a b es 1, as´ı que ese valor es el que se asigna tambi´en a a. La asignaci´on con una operaci´on ((op)) hace que a la variable de la izquierda se le asigne el resultado de operar con ((op)) su valor con el operando derecho. Por ejemplo, a /= 3 es equivalente a a = a / 3. Este tipo de asignaci´on con operaci´on recibe el nombre de asignaci´on aumentada. Operador de tama˜no sizeof. El operador sizeof puede aplicarse a un nombre de tipo (encerrado entre par´entesis) o a un identificador de variable. En el primer caso devuelve el n´umero de bytes que ocupa en memoria una variable de ese tipo, y en el segundo, el n´umero de bytes que ocupa esa variable. Si a es una variable de tipo char, tanto sizeof(a) como sizeof(char) devuelven el valor 1. Ojo: recuerda que ’a’ es literal entero, as´ı que sizeof(’a’) vale 4. Operadores de coerci´on o conversi´on de tipos (en ingl´es ((type casting operator))). Pue- des convertir un valor de un tipo de datos a otro que sea ((compatible)). Para ello dispones de operadores de la forma (tipo), donde tipo es int, float, etc. Por ejemplo, si deseas efectuar una divisi´on entre enteros que no pierda decimales al convertir el resultado a un flotante, puedes hacerlo como te muestra este programa: 1 #include <stdio.h> 2 3 int main(void) 4 { 5 float x; 6 int a = 1, b = 2; 7 8 x = a / (float) b; 9 } En este ejemplo, hemos convertido el valor de b a un float antes de efectuar la divisi´on. Es similar a la funci´on float de Python, s´olo que en Python se hac´ıa la conversi´on con una llamada a funci´on como float(b), y aqu´ı utilizamos un operador prefijo: (float) b. Es una notaci´on bastante extra˜na, as´ı que es probable que te confunda durante un tiempo. En la siguiente secci´on abundaremos en la cuesti´on de la conversi´on de tipos en C. Operador condicional (?:). Este operador no tiene correlato en Python. Hay tres operandos: una condici´on y dos ex- presiones14 . El resultado de la operaci´on es el valor de la primera expresi´on si la condici´on es cierta y el valor de la segunda si es falsa. Por ejemplo, la asignaci´on a = (x > 10) ? 100 : 200 14Lo cierto es que hay tres expresiones, pues la comparaci´on no es m´as que una expresi´on. Si dicha expresi´on devuelve el valor 0, se interpreta el resultado como ((falso)); en caso contrario, el resultado es ((cierto)). 38 Introducci´on a la Programaci´on con C
  • 45. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C almacena en a el valor 100 o 200, dependiendo de si x es o no es mayor que 10. Es equivalente a este fragmento de programa: if (x > 10) a = 100; else a = 200; Operadores de incremento/decremento Preincremento (++ en forma prefija), postincre- mento (++ en forma postfija), predecremento (-- en forma prefija), postdecremento (-- en forma postfija). Estos operadores no tienen equivalente inmediato en Python. Los operadores de incre- mento y decremento pueden ir delante de una variable (forma prefija) o detr´as (forma postfija). La variable debe ser de tipo entero (int, unsigned int, char, etc.). En ambos casos incrementan (++) o decrementan (--) en una unidad el valor de la variable entera. Si i vale 1, valdr´a 2 despu´es de ejecutar ++i o i++, y valdr´a 0 despu´es de ejecutar --i o i--. Hay una diferencia importante entre aplicar estos operadores en forma prefija o sufija. La expresi´on i++ primero se eval´ua como el valor actual de i y despu´es hace que i incremente su valor en una unidad. La expresi´on ++i primero incrementa el valor de i en una unidad y despu´es se eval´ua como el valor actual (que es el que resulta de efectuar el incremento). Si el operador se est´a aplicando en una expresi´on, esta diferencia tiene importancia. Su- pongamos que i vale 1 y que evaluamos esta asignaci´on: a = i++; La variable a acaba valiendo 1 e i acaba valiendo 2. F´ıjate: al ser un postincremento, primero se devuelve el valor de i, que se asigna a a, y despu´es se incrementa i. Al ejecutar esta otra asignaci´on obtenemos un resultado diferente: a = ++i; Tanto a como i acaban valiendo 2. El operador de preincremento primero asigna a i su valor actual incrementado en una unidad y despu´es devuelve ese valor (ya incrementado), que es lo que finalmente estamos asignando a a. Lo mismo ocurre con los operadores de pre y postdecremento, pero, naturalmente, decre- mentado el valor en una unidad en lugar de incrementarlo. Que haya operadores de pre y postincremento (y pre y postdecremento) te debe parecer una rareza excesiva y pensar´as que nunca necesitar´as hilar tan fino. Si es as´ı, te equivocas: en los pr´oximos cap´ıtulos usaremos operadores de incremento y necesitaremos escoger entre preincremento y postincremento. Nos dejamos en el tintero unos pocos operadores (((())), (([])), ((->)), ((.)), ((,)), y ((*)) unario. Los presentaremos cuando convenga y sepamos algo m´as de C. C++ Ya debes entender de d´onde viene el nombre C++: es un C ((incrementado)), o sea, mejorado. En realidad C++ es mucho m´as que un C con algunas mejoras: es un lenguaje orientado a objetos, as´ı que facilita el dise˜no de programas siguiendo una filosof´ıa diferente de la propia de los lenguajes imperativos y procedurales como C. Pero esa es otra historia. En esta tabla te relacionamos todos los operadores (incluso los que a´un no te hemos presen- tado con detalle) ordenados por precedencia (de mayor a menor) y con su aridad (n´umero de operandos) y asociatividad: Introducci´on a la Programaci´on con C 39
  • 46. 1.12 Expresiones Operador Aridad Asociatividad () [] -> . ++postfijo--postfijo 2 izquierda ! ~ + - sizeof * & (tipo) ++prefijo--prefijo 1 derecha * / % 2 izquierda + - 2 izquierda << >> 2 izquierda < <= > >= 2 izquierda == != 2 izquierda & 2 izquierda ^ 2 izquierda | 2 izquierda && 2 izquierda || 2 izquierda ?: 3 izquierda = += -= *= /= %= <<= >>= &= ^= |= 2 derecha , 2 izquierda . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 19 Sean a, b y c tres variables de tipo int cuyos valores actuales son 0, 1 y 2, respectivamente. ¿Qu´e valor tiene cada variable tras ejecutar esta secuencia de asignaciones? 1 a = b++ - c--; 2 a += --b; 3 c *= a + b; 4 a = b | c; 5 b = (a > 0) ? ++a : ++c; 6 b <<= a = 2; 7 c >>= a == 2; 8 a += a = b + c; · 20 ¿Qu´e hace este programa? ternario.c ternario.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a, b, c, r; 6 7 printf ("Dame un valor entero: "); scanf ("%d", &a); 8 printf ("Dame otro valor entero: "); scanf ("%d", &b); 9 printf ("Y uno m´as: "); scanf ("%d", &c); 10 11 r = (a < b) ? ( (a < c) ? a : c ) : ( (b < c) ? b : c ); 12 13 printf ("Resultado: %dn", r); 14 15 return 0; 16 } · 21 Haz un programa que solicite el valor de x y muestre por pantalla el resultado de evaluar x4 − x2 + 1. (Recuerda que en C no hay operador de exponenciaci´on.) · 22 Dise˜na un programa C que solicite la longitud del lado de un cuadrado y muestre por pantalla su per´ımetro y su ´area. · 23 Dise˜na un programa C que solicite la longitud de los dos lados de un rect´angulo y muestre por pantalla su per´ımetro y su ´area. · 24 Este programa C es problem´atico: un misterio.c un misterio.c 1 #include <stdio.h> 40 Introducci´on a la Programaci´on con C
  • 47. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C 2 3 int main(void) 4 { 5 int a, b; 6 7 a = 2147483647; 8 b = a + a; 9 printf ("%dn", a); 10 printf ("%dn", b); 11 return 0; 12 } Al compilarlo y ejecutarlo hemos obtenido la siguiente salida por pantalla: 2147483647 -2 ¿Qu´e ha ocurrido? · 25 Dise˜na un programa C que solicite el radio r de una circunferencia y muestre por pantalla su per´ımetro (2πr) y su ´area (πr2 ). · 26 Si a es una variable de tipo char con el valor 127, ¿qu´e vale ~a? ¿Y qu´e vale !a? Y si a es una variable de tipo unsigned int con el valor 2147483647, ¿qu´e vale ~a? ¿Y qu´e vale !a? · 27 ¿Qu´e resulta de evaluar cada una de estas dos expresiones? a) 1 && !!!(0 || 1) || !(0 || 1) b) 1 & ~~~(0 | 1) | ~(0 | 1) · 28 ¿Por qu´e si a es una variable entera a / 2 proporciona el mismo resultado que a >> 1? ¿Con qu´e operaci´on de bits puedes calcular a * 2? ¿Y a / 32? ¿Y a * 128? · 29 ¿Qu´e hace este programa? swap.c swap.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 unsigned char a, b; 6 printf ("Introduce el valor de a (entre 0 y 255): "); scanf ("%hhu",&a); 7 printf ("Introduce el valor de b (entre 0 y 255): "); scanf ("%hhu",&b); 8 9 a ^= b; 10 b ^= a; 11 a ^= b; 12 13 printf ("Valor de a: %hhun", a); 14 printf ("Valor de b: %hhun", b); 15 16 return 0; 17 } (Nota: la forma en que hace lo que hace viene de un viejo truco de la programaci´on en ensamblador, donde hay ricos juegos de instrucciones para la manipulaci´on de datos bit a bit.). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.13. Conversi´on impl´ıcita y expl´ıcita de tipos El sistema de tipos escalares es m´as r´ıgido que el de Python, aunque m´as rico. Cuando se eval´ua una expresi´on y el resultado se asigna a una variable, has de tener en cuenta el tipo de todos los operandos y tambi´en el de la variable en la que se almacena. Ilustraremos el comportamiento de C con fragmentos de programa que utilizan estas varia- bles: Introducci´on a la Programaci´on con C 41
  • 48. 1.13 Conversi´on impl´ıcita y expl´ıcita de tipos ¿5 > 3 > 2? Recuerda que en Python pod´ıamos combinar operadores de comparaci´on para formar ex- presiones como 5 > 3 > 2. Esa, en particular, se eval´ua a True, pues 5 es mayor que 3 y 3 es mayor que 2. C tambi´en acepta esa expresi´on, pero con un significado completamente diferente basado en la asociatividad por la izquierda del operador >: en primer lugar eval´ua la subexpresi´on 5 > 3, que proporciona el valor ((cierto)); pero como ((cierto)) es 1 (valor por defecto) y 1 no es mayor que 2, el resultado de la evaluaci´on es 0, o sea, ((falso)). ¡Ojo con la interferencia entre ambos lenguajes! Problemas como ´este surgir´an con fre- cuencia cuando aprendas nuevos lenguajes: construcciones que significan algo en el lenguaje que conoces bien tienen un significado diferente en el nuevo. char c; int i; float x; Si asignas a un entero int el valor de un entero m´as corto, como un char, el entero corto promociona a un entero int autom´aticamente. Es decir, es posible efectuar esta asignaci´on sin riesgo alguno: i = c; Podemos igualmente asignar un entero int a un char. C se encarga de hacer la conversi´on de tipos pertinente: c = i; Pero, ¿c´omo? ¡En un byte (lo que ocupa un char) no caben cuatro (los que ocupa un int)! C toma los 8 bits menos significativos de i y los almacena en c, sin m´as. La conversi´on funciona correctamente, es decir, preserva el valor, s´olo si el n´umero almacenado en i est´a comprendido entre −128 y 127. Observa este programa: conversion delicada.c conversion delicada.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a, b; 6 char c, d; 7 8 a = 512; 9 b = 127; 10 c = a; 11 d = b; 12 printf ("%hhd %hhdn", c, d); 13 14 return 0; 15 } Produce esta salida por pantalla: 0 127 ¿Por qu´e el primer resultado es 0? El valor 512, almacenado en una variable de tipo int, se representa con este patr´on de bits: 00000000000000000000001000000000. Sus 8 bits menos significativos se almacenan en la variable c al ejecutar la asignaci´on c = a, es decir, c almacena el patr´on de bits 00000000, que es el valor decimal 0. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 30 ¿Qu´e mostrar´a por pantalla este programa? otra conversion delicada.c otra conversion delicada.c 1 #include <stdio.h> 42 Introducci´on a la Programaci´on con C
  • 49. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C 2 3 int main(void) 4 { 5 int a, b; 6 char c, d; 7 unsigned char e, f; 8 9 a = 384; 10 b = 256; 11 c = a; 12 d = b; 13 e = a; 14 f = b; 15 printf ("%hhd %hhdn", c, d); 16 printf ("%hhu %hhun", e, f); 17 18 return 0; 19 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Si asignamos un entero a una variable flotante, el entero promociona a su valor equivalente en coma flotante. Por ejemplo, esta asignaci´on almacena en x el valor 2.0 (no el entero 2). x = 2; Si asignamos un valor flotante a un entero, el flotante se convierte en su equivalente entero (¡si lo hay!). Por ejemplo, la siguiente asignaci´on almacena el valor 2 en i (no el flotante 2.0). i = 2.0; Y esta otra asignaci´on almacena en i el valor 0: i = 0.1; . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 31 ¿Qu´e valor se almacena en las variables i (de tipo int) y x (de tipo float) tras ejecutar cada una de estas sentencias? a) i = 2; b) i = 1 / 2; c) i = 2 / 4; d) i = 2.0 / 4; e) x = 2.0 / 4.0; f) x = 2.0 / 4; g) x = 2 / 4; h) x = 1 / 2; . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Aunque C se encarga de efectuar impl´ıcitamente muchas de las conversiones de tipo, pue- de que en ocasiones necesites indicar expl´ıcitamente una conversi´on de tipo. Para ello, debes preceder el valor a convertir con el tipo de destino encerrado entre par´entesis. As´ı: i = (int) 2.3; En este ejemplo da igual poner (int) que no ponerlo: C hubiera hecho la conversi´on impl´ıci- tamente. El t´ermino (int) es el operador de conversi´on a enteros de tipo int. Hay un operador de conversi´on para cada tipo: (char), (unsigned int) (float), etc. . . Recuerda que el s´ımbolo (tipo) es un operador unario conocido como operador de coerci´on o conversi´on de tipos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 32 ¿Qu´e valor se almacena en las variables i (de tipo int) y x (de tipo float) tras ejecutar estas sentencias? a) i = (float) 2; b) i = 1 / (float) 2; c) i = (int) (2 / 4); d) i = (int) 2. / (float) 4; e) x = 2.0 / (int) 4.0; f) x = (int) 2.0 / 4; g) x = (int) (2.0 / 4); h) x = 2 / (float) 4; i) x = (float) (1 / 2); j) x = 1 / (float) 2; . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Introducci´on a la Programaci´on con C 43
  • 50. 1.14 Las directivas y el preprocesador 1.14. Las directivas y el preprocesador Las l´ıneas que empiezan con una palabra predecida por el car´acter # son especiales. Las palabras que empiezan con # se denominan directivas. El compilador no llega a ver nunca las l´ıneas que empiezan con una directiva. ¿Qu´e queremos decir exactamente con que no llega a verlas? El compilador gcc es, en realidad, un programa que controla varias etapas en el proceso de traducci´on de C a c´odigo de m´aquina. De momento, nos interesa considerar dos de ellas: el preprocesador, y el traductor de C a c´odigo de m´aquina (el compilador propiamente dicho). programa.c Preprocesador Compilador programa El preprocesador es un programa independiente, aunque es infrecuente invocarlo directamente. El preprocesador del compilador gcc se llama cpp. Las directivas son analizadas e interpretadas por el preprocesador. La directiva #include seguida del nombre de un fichero (entre los caracteres < y >) hace que el preprocesador sustituya la l´ınea en la que aparece por el contenido ´ıntegro del fichero (en ingl´es ((include)) significa ((incluye))). El compilador, pues, no llega a ver la directiva, sino el resultado de su sustituci´on. Nosotros s´olo estudiaremos, de momento, dos directivas: #define, que permite definir constantes, e #include, que permite incluir el contenido de un fichero y que se usa para importar funciones, variables, constantes, etc. de bibliotecas. 1.15. Constantes 1.15.1. Definidas con la directiva define Una diferencia de C con respecto a Python es la posibilidad que tiene el primero de definir constantes. Una constante es, en principio15 , una variable cuyo valor no puede ser modificado. Las constantes se definen con la directiva #define. As´ı: #define CONSTANTE valor Cada l´ınea #define s´olo puede contener el valor de una constante. Por ejemplo, podemos definir los valores aproximados de π y del n´umero e as´ı: #define PI 3.1415926535897931159979634685442 #define E 2.7182818284590450907955982984276 Intentar asignar un valor a PI o a E en el programa produce un error que detecta el compi- lador16 . Observa que no hay operador de asignaci´on entre el nombre de la constante y su valor y que la l´ınea no acaba con punto y coma17 . Es probable que cometas m´as de una vez el error de escribir el operador de asignaci´on o el punto y coma. No es obligatorio que el nombre de la constante se escriba en may´usculas, pero s´ı un convenio ampliamente adoptado. 1.15.2. Definidas con el adjetivo const C99 propone una forma alternativa de definir constantes mediante una nueva palabra reservada: const. Puedes usar const delante del tipo de una variable inicializada en la declaraci´on para indicar que su valor no se modificar´a nunca. 15Lo de ((en principio)) est´a justificado. No es cierto que las constantes de C sean variables. Lee el cuadro titulado ((El preprocesador y las constantes)) para saber qu´e son exactamente. 16¿Has le´ıdo ya el cuadro ((El preprocesador y las constantes))? 17¿A qu´e esperas para leer el cuadro ((El preprocesador y las constantes))? 44 Introducci´on a la Programaci´on con C
  • 51. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C El preprocesador y las constantes Como te dijimos antes, el compilador de C no compila directamente nuestros ficheros con extensi´on ((.c)). Antes de compilarlos, son tratados por un programa al que se conoce como preprocesador. El preprocesador (que en Unix suele ser el programa cpp, por ((C preprocessor))) procesa las denominadas directivas (l´ıneas que empiezan con #). Cuando el preprocesador encuentra la directiva #define, la elimina, pero recuerda la asociaci´on establecida entre un identificador y un texto; cada vez que encuentra ese identificador en el programa, lo sustituye por el texto. Un ejemplo ayudar´a a entender el porqu´e de algunos errores misteriosos de C cuando se trabaja con constantes. Al compilar este programa: preprocesar.c preprocesar.c 1 #define PI 3.14 2 3 int main(void) 4 { 5 int a = PI; 6 return 0; 7 } el preprocesador lo transforma en este otro programa (sin modificar nuestro fichero). Puedes comprobarlo invocando directamente al preprocesador: $ cpp -P preprocesar.c El resultado es esto: 1 int main(void) 2 { 3 int a = 3.14; 4 return 0; 5 } Como puedes ver, una vez ((preprocesado)), no queda ninguna directiva en el programa y la aparici´on del identificador PI ha sido sustituida por el texto 3.14. Un error t´ıpico es confundir un #define con una declaraci´on normal de variables y, en consecuencia, poner una asignaci´on entre el identificador y el valor: 1 #define PI = 3.14 2 3 int main(void) 4 { 5 int a = PI; 6 return 0; 7 } El programa resultante es incorrecto. ¿Por qu´e? El compilador ve el siguiente programa tras ser preprocesado: 1 int main(void) 2 { 3 int a = = 3.14; 4 return 0; 5 } ¡La tercera l´ınea del programa resultante no sigue la sintaxis del C! constante.c constante.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 const float pi = 3.14; 6 float r, a; 7 Introducci´on a la Programaci´on con C 45
  • 52. 1.15 Constantes 8 printf ("Radio: "); 9 scanf ("%f", &r); 10 11 a = pi * r * r; 12 13 printf ("´Area: %fn", a); 14 15 return 0; 16 } Pero la posibilidad de declarar constantes con const no nos libra de la directiva define, pues no son de aplicaci´on en todo lugar donde conviene usar una constante. M´as adelante, al estudiar la declaraci´on de vectores, nos referiremos nuevamente a esta cuesti´on. 1.15.3. Con tipos enumerados Es frecuente definir una serie de constantes con valores consecutivos. Imagina una aplicaci´on en la que escogemos una opci´on de un men´u como ´este: 1) Cargar registros 2) Guardar registros 3) A~nadir registro 4) Borrar registro 5) Modificar registro 6) Buscar registro 7) Finalizar Cuando el usuario escoge una opci´on, la almacenamos en una variable (llam´emosla opcion) y seleccionamos las sentencias a ejecutar con una serie de comparaciones como las que se muestran aqu´ı esquem´aticamente18 : if (opcion == 1) { C´odigo para cargar registros } else if (opcion == 2) { C´odigo para guardar registros } else if (opcion == 3) { ... El c´odigo resulta un tanto ilegible porque no vemos la relaci´on entre los valores num´ericos y las opciones de men´u. Es frecuente no usar los literales num´ericos y recurrir a constantes: #define CARGAR 1 #define GUARDAR 2 #define ANYADIR 3 #define BORRAR 4 #define MODIFICAR 5 #define BUSCAR 6 #define FINALIZAR 7 ... if (opcion == CARGAR) { C´odigo para cargar registros } else if (opcion == GUARDAR) { C´odigo para guardar registros } else if (opcion == ANYADIR) { ... 18M´as adelante estudiaremos una estructura de selecci´on que no es if y que se usa normalmente para especificar este tipo de acciones. 46 Introducci´on a la Programaci´on con C
  • 53. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C Puedes ahorrarte la retah´ıla de #defines con los denominados tipos enumerados. Un tipo enumerado es un conjunto de valores ((con nombre)). F´ıjate en este ejemplo: enum { Cargar=1, Guardar, Anyadir, Borrar, Modificar, Buscar, Finalizar }; ... if (opcion == Cargar) { C´odigo para cargar registros } else if (opcion == Guardar) { C´odigo para guardar registros } else if (opcion == Anyadir) { ... La primera l´ınea define los valores Cargar, Guardar, . . . como una sucesi´on de valores correlativos. La asignaci´on del valor 1 al primer elemento de la enumeraci´on hace que la sucesi´on empiece en 1. Si no la hubi´esemos escrito, la sucesi´on empezar´ıa en 0. Es habitual que los enum aparezcan al principio del programa, tras la aparici´on de los #include y #define. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 33 ¿Qu´e valor tiene cada identificador de este tipo enumerado? enum { Primera=’a’, Segunda, Tercera, Penultima=’y’, Ultima }; (No te hemos explicado qu´e hace la segunda asignaci´on. Comprueba que la explicaci´on que das es correcta con un programa que muestre por pantalla el valor de cada identificador.) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Los tipos enumerados sirven para algo m´as que asignar valores a opciones de men´u. Es posible definir identificadores con diferentes valores para series de elementos como los d´ıas de la semana, los meses del a˜no, etc. enum { Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo }; enum { Invierno, Primavera, Verano, Otonyo }; enum { Rojo, Verde, Azul }; 1.16. Las bibliotecas (m´odulos) se importan con #include En C, los m´odulos reciben el nombre de bibliotecas (o librer´ıas, como traducci´on fon´eticamente similar del ingl´es library). La primera l´ınea de sumatorio.c es ´esta: 1 #include <stdio.h> Con ella se indica que el programa hace uso de una biblioteca cuyas funciones, variables, tipos de datos y constantes est´an declaradas en el fichero stdio.h, que es abreviatura de ((standard input/output)) (entrada/salida est´andar). En particular, el programa sumatorio.c usa las fun- ciones printf y scanf de stdio.h. Los ficheros con extensi´on ((.h)) se denominan ficheros cabecera (la letra h es abreviatura de ((header)), que en ingl´es significa ((cabecera))). A diferencia de Python, C no permite importar un subconjunto de las funciones propor- cionadas por una biblioteca. Al hacer #include de una cabecera se importan todas sus fun- ciones, tipos de datos, variables y constantes. Es como si en Python ejecutaras la sentencia from m´odulo import *. Normalmente no basta con incluir un fichero de cabecera con #include para poder compilar un programa que utiliza bibliotecas. Es necesario, adem´as, compilar con opciones especiales. Abundaremos sobre esta cuesti´on inmediatamente, al presentar la librer´ıa matem´atica. 1.16.1. La biblioteca matem´atica Podemos trabajar con funciones matem´aticas incluyendo math.h en nuestros programas. La tabla 1.1 relaciona algunas de las funciones que ofrece la biblioteca matem´atica. Introducci´on a la Programaci´on con C 47
  • 54. 1.16 Las bibliotecas (m´odulos) se importan con #include Funci´on C Funci´on matem´atica sqrt(x) ra´ız cuadrada de x sin(x) seno de x cos(x) coseno de x tan(x) tangente de x asin(x) arcoseno de x acos(x) arcocoseno de x atan(x) arcotangente de x exp(x) el n´umero e elevado a x exp10(x) 10 elevado a x log(x) logaritmo en base e de x log10(x) logaritmo en base 10 de x log2(x) logaritmo en base 2 de x pow(x, y) x elevado a y fabs(x) valor absoluto de x round(x) redondeo al entero m´as pr´oximo a x ceil(x) redondeo superior de x floor(x) redondeo inferior de x Tabla 1.1: Algunas funciones matem´aticas disponibles en la biblioteca math.h. Todos los argumentos de las funciones de math.h son de tipo flotante.19 La biblioteca matem´atica tambi´en ofrece algunas constantes matem´aticas predefinidas. Te relacionamos algunas en la tabla 1.2. Constante Valor M_E una aproximaci´on del n´umero e M_PI una aproximaci´on del n´umero π M_PI_2 una aproximaci´on de π/2 M_PI_4 una aproximaci´on de π/4 M_1_PI una aproximaci´on de 1/π M_SQRT2 una aproximaci´on de √ 2 M_LOG2E una aproximaci´on de log2 e M_LOG10E una aproximaci´on de log10 e Tabla 1.2: Algunas constantes disponibles en la biblioteca math.h. No basta con escribir #include <math.h> para poder usar las funciones matem´aticas: has de compilar con la opci´on -lm: $ gcc programa.c -lm -o programa ¿Por qu´e? Cuando haces #include, el preprocesador introduce un fragmento de texto que dice qu´e funciones pasan a estar accesibles, pero ese texto no dice qu´e hace cada funci´on y c´omo lo hace (con qu´e instrucciones concretas). Si compilas sin -lm, el compilador se ((quejar´a)): $ gcc programa.c -o programa /tmp/ccm1nE0j.o: In function ‘main’: /tmp/ccm1nE0j.o(.text+0x19): undefined reference to ‘sqrt’ collect2: ld returned 1 exit status El mensaje advierte de que hay una ((referencia indefinida a sqrt)). En realidad no se est´a ((quejando)) el compilador, sino otro programa del que a´un no te hemos dicho nada: el enlazador (en ingl´es, ((linker))). El enlazador es un programa que detecta en un programa las llamadas a funci´on no definidas en un programa C y localiza la definici´on de las funciones (ya compiladas) en bibliotecas. El fichero math.h que inclu´ımos con #define contiene la cabecera de las funciones 19Lo cierto es que son de tipo double (v´ease el ap´endice A), pero no hay problema si las usas con valores y variables de tipo float, ya que hay conversi´on autom´atica de tipos. 48 Introducci´on a la Programaci´on con C
  • 55. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C matem´aticas, pero no su cuerpo. El cuerpo de dichas funciones, ya compilado (es decir, en c´odigo de m´aquina), reside en otro fichero: /usr/lib/libm.a. ¿Para qu´e vale el fichero math.h si no tiene el cuerpo de las funciones? Para que el compilador compruebe que estamos usando correctamente las funciones (que suministramos el n´umero de argumentos adecuado, que su tipo es el que debe ser, etc.). Una vez que se comprueba que el programa es correcto, se procede a generar el c´odigo de m´aquina, y ah´ı es necesario ((pegar)) (((enlazar))) el c´odigo de m´aquina de las funciones matem´aticas que hemos utilizado. El cuerpo ya compilado de sqrt, por ejemplo, se encuentra en /usr/lib/libm.a (libm es abreviatura de ((math library))). El enlazador es el programa que ((enlaza)) el c´odigo de m´aquina de nuestro programa con el c´odigo de m´aquina de las bibliotecas que usamos. Con la opci´on -lm le indicamos al enlazador que debe resolver las referencias indefinidas a funciones matem´aticas utilizando /usr/lib/libm.a. La opci´on -lm evita tener que escribir /usr/lib/libm.a al final. Estas dos invocaciones del compilador son equivalentes: $ gcc programa.c -o programa -lm $ gcc programa.c -o programa /usr/lib/libm.a El proceso completo de compilaci´on cuando enlazamos con /usr/lib/libm.a puede repre- sentarse gr´aficamente as´ı: programa.c Preprocesador Compilador Enlazador programa /usr/lib/libm.a . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 34 Dise˜na un programa C que solicite la longitud de los tres lados de un tri´angulo (a, b y c) y muestre por pantalla su per´ımetro y su ´area ( s(s − a)(s − b)(s − c), donde s = (a+b+c)/2.). Compila y ejecuta el programa. · 35 Dise˜na un programa C que solicite el radio r de una circunferencia y muestre por pantalla su per´ımetro (2πr) y su ´area (πr2 ). Utiliza la aproximaci´on a π predefinida en la biblioteca matem´atica. Compila y ejecuta el programa. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.17. Estructuras de control Las estructuras de control de C son parecidas a las de Python. Bueno, hay alguna m´as y todas siguen unas reglas sint´acticas diferentes. Empecemos estudiando las estructuras de control condicionales. 1.17.1. Estructuras de control condicionales La sentencia de selecci´on if La estructura de control condicional fundamental es el if. En C se escribe as´ı: if (condici´on) { sentencias } Los par´entesis que encierran a la condici´on son obligatorios. Como en Python no lo son, es f´acil que te equivoques por no ponerlos. Si el bloque de sentencias consta de una sola sentencia, no es necesario encerrarla entre llaves: if (condici´on) sentencia; Introducci´on a la Programaci´on con C 49
  • 56. 1.17 Estructuras de control La sentencia de selecci´on if-else Hay una forma if-else, como en Python: if (condici´on) { sentencias_si } else { sentencias_no } Si uno de los bloques s´olo tiene una sentencia, generalmente puedes eliminar las llaves: if (condici´on) sentencia_si; else { sentencias_no } if (condici´on) { sentencias_si } else sentencia_no; if (condici´on) sentencia_si; else sentencia_no; Ojo: la indentaci´on no significa nada para el compilador. La ponemos ´unicamente para facilitar la lectura. Pero si la indentaci´on no significa nada nos enfrentamos a un problema de ambig¨uedad con los if anidados: if (condici´on) if (otra_condici´on) { sentencias_si } else { // ??? ??? sentencias_no } ¿A cu´al de los dos if pertenece el else? ¿Har´a el compilador de C una interpretaci´on como la que sugiere la indentaci´on en el ´ultimo fragmento o como la que sugiere este otro?: if (condici´on) if (otra_condici´on) { sentencias_si } else { // ??? ??? sentencias_no } C rompe la ambig¨uedad trabajando con esta sencilla regla: el else pertenece al if ((libre)) m´as cercano. Si quisi´eramos expresar la primera estructura, deber´ıamos a˜nadir llaves para determinar completamente qu´e bloque est´a dentro de qu´e otro: if (condici´on) { if (otra_condici´on) { sentencias_si } } else { sentencias_no } El if externo contiene una sola sentencia (otro if) y, por tanto, las llaves son redundantes; pero hacen evidente que el else va asociado a la condici´on exterior. 50 Introducci´on a la Programaci´on con C
  • 57. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C No hay sentencia elif: la combinaci´on else if C no tiene una estructura elif como la de Python, pero tampoco la necesita. Puedes usar else if donde hubieras puesto un elif en Python: if (condici´on) { sentencias_si } else if (condici´on2) { sentencias_si2 } else if (condici´on3) { sentencias_si3 } else { sentencias_no } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 36 Dise˜na un programa C que pida por teclado un n´umero entero y diga si es par o impar. · 37 Dise˜na un programa que lea dos n´umeros enteros y muestre por pantalla, de estos tres mensajes, el que convenga: ((El segundo es el cuadrado exacto del primero.)), ((El segundo es menor que el cuadrado del primero.)), ((El segundo es mayor que el cuadrado del primero.)). · 38 Tambi´en en C es problem´atica la divisi´on por 0. Haz un programa C que resuelva la ecuaci´on ax+b = 0 solicitando por teclado el valor de a y b (ambos de tipo float). El programa detectar´a si la ecuaci´on no tiene soluci´on o si tiene infinitas soluciones y, en cualquiera de los dos casos, mostrar´a el pertinente aviso. · 39 Dise˜na un programa que solucione ecuaciones de segundo grado. El programa detectar´a y tratar´a por separado las siguientes situaciones: la ecuaci´on tiene dos soluciones reales; la ecuaci´on tiene una ´unica soluci´on real; la ecuaci´on no tiene soluci´on real; la ecuaci´on tiene infinitas soluciones. · 40 Realiza un programa que proporcione el desglose en billetes y monedas de una cantidad exacta de euros. Hay billetes de 500, 200, 100, 50, 20, 10 y 5 euros y monedas de 1 y 2 euros. Por ejemplo, si deseamos conocer el desglose de 434 euros, el programa mostrar´a por pantalla el siguiente resultado: 2 billetes de 200 euros. 1 billete de 20 euros. 1 billete de 10 euros. 2 monedas de 2 euros. Observa que la palabra ((billete)) (y ((moneda))) concuerda en n´umero con la cantidad de billetes (o monedas) y que si no hay piezas de un determinado tipo (en el ejemplo, de 1 euro), no muestra el mensaje correspondiente. · 41 Dise˜na un programa C que lea un car´acter cualquiera desde el teclado, y muestre el mensaje ((Es una MAY´USCULA.)) cuando el car´acter sea una letra may´uscula y el mensaje ((Es una MIN´USCULA.)) cuando sea una min´uscula. En cualquier otro caso, no mostrar´a mensaje alguno. (Considera ´unicamente letras del alfabeto ingl´es.) Introducci´on a la Programaci´on con C 51
  • 58. 1.17 Estructuras de control · 42 Dise˜na un programa que lea cinco n´umeros enteros por teclado y determine cu´al de los cuatro ´ultimos n´umeros es m´as cercano al primero. (Por ejemplo, si el usuario introduce los n´umeros 2, 6, 4, 1 y 10, el programa responder´a que el n´umero m´as cercano al 2 es el 1.) · 43 Dise˜na un programa que, dado un n´umero entero, determine si ´este es el doble de un n´umero impar. (Ejemplo: 14 es el doble de 7, que es impar.) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La sentencia de selecci´on switch Hay una estructura condicional que no existe en Python: la estructura de selecci´on m´ultiple. Esta estructura permite seleccionar un bloque de sentencias en funci´on del valor de una expresi´on (t´ıpicamente una variable). 1 switch (expresi´on) { 2 case valor1: 3 sentencias 4 break; 5 case valor2: 6 sentencias 7 break; 8 ... 9 default: 10 sentencias 11 break; 12 } El fragmento etiquetado con default es opcional. Para ilustrar el uso de switch, nada mejor que un programa que muestra algo por pantalla en funci´on de la opci´on seleccionada de un men´u: menu.c menu.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int opcion; 6 7 printf ("1) Saludan"); 8 printf ("2) Desp´ıdeten"); 9 scanf ("%d", &opcion); 10 switch (opcion) { 11 case 1: 12 printf ("Holan"); 13 break; 14 case 2: 15 printf ("Adi´osn"); 16 break; 17 default: 18 printf ("Opci´on no v´alidan"); 19 break; 20 } 21 return 0; 22 } Aunque resulta algo m´as elegante esta otra versi´on, que hace uso de tipos enumerados: menu 1.c menu.c 1 #include <stdio.h> 2 3 enum { Saludar=1, Despedirse }; 4 52 Introducci´on a la Programaci´on con C
  • 59. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C 5 int main(void) 6 { 7 int opcion; 8 9 printf ("1) Saludan"); 10 printf ("2) Desp´ıdeten"); 11 scanf ("%d", &opcion); 12 switch (opcion) { 13 case Saludar : 14 printf ("Holan"); 15 break; 16 case Despedirse : 17 printf ("Adi´osn"); 18 break; 19 default: 20 printf ("Opci´on no v´alidan"); 21 break; 22 } 23 return 0; 24 } Un error t´ıpico al usar la estructura switch es olvidar el break que hay al final de cada opci´on. Este programa, por ejemplo, presenta un comportamiento curioso: menu2.c E menu2.c E 1 #include <stdio.h> 2 3 enum { Saludar=1, Despedirse }; 4 5 int main(void) 6 { 7 int opcion; 8 9 printf ("1) Saludan"); 10 printf ("2) Desp´ıdeten"); 11 scanf ("%d", &opcion); 12 switch (opcion) { 13 case Saludar: 14 printf ("Holan"); 15 case Despedirse: 16 printf ("Adi´osn"); 17 default: 18 printf ("Opci´on no v´alidan"); 19 } 20 return 0; 21 } Si seleccionas la opci´on 1, no sale un ´unico mensaje por pantalla, ¡salen tres: Hola, Adi´os y Opci´on no v´alida! Y si seleccionas la opci´on 2, ¡salen dos mensajes: Adi´os y Opci´on no v´alida! Si no hay break, el flujo de control que entra en un case ejecuta las acciones asociadas al siguiente case, y as´ı hasta encontrar un break o salir del switch por la ´ultima de sus l´ıneas. El compilador de C no se˜nala la ausencia de break como un error porque, de hecho, no lo es. Hay casos en los que puedes explotar a tu favor este curioso comportamiento del switch: menu3.c menu3.c 1 #include <stdio.h> 2 3 enum { Saludar=1, Despedirse, Hola, Adios }; 4 5 int main(void) 6 { 7 int opcion; 8 9 printf ("1) Saludan"); Introducci´on a la Programaci´on con C 53
  • 60. 1.17 Estructuras de control 10 printf ("2) Desp´ıdeten"); 11 printf ("3) Di holan"); 12 printf ("4) Di adi´osn"); 13 scanf ("%d", &opcion); 14 switch (opcion) { 15 case Saludar: 16 case Hola: 17 printf ("Holan"); 18 break; 19 case Despedirse: 20 case Adios: 21 printf ("Adi´osn"); 22 break; 23 default: 24 printf ("Opci´on no v´alidan"); 25 break; 26 } 27 return 0; 28 } ¿Ves por qu´e? 1.17.2. Estructuras de control iterativas El bucle while El bucle while de Python se traduce casi directamente a C: while (condici´on) { sentencias } Nuevamente, los par´entesis son obligatorios y las llaves pueden suprimirse si el bloque contiene una sola sentencia. Veamos un ejemplo de uso: un programa que calcula xn para x y n enteros: potencia.c potencia.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int x, n, i, r; 6 7 printf ("x: "); scanf ("%d", &x); 8 printf ("n: "); scanf ("%d", &n); 9 r = 1; 10 i = 0; 11 while (i < n) { 12 r *= x; 13 i++; 14 } 15 printf ("%d**%d = %dn", x, n, r); 16 17 return 0; 18 } El bucle do-while Hay un bucle iterativo que Python no tiene: el do-while: do { sentencias } while (condici´on); 54 Introducci´on a la Programaci´on con C
  • 61. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C El bucle do-while eval´ua la condici´on tras cada ejecuci´on de su bloque, as´ı que es seguro que ´este se ejecuta al menos una vez. Podr´ıamos reescribir sumatorio.c para usar un bucle do-while: sumatorio 2.c sumatorio.c 1 #include <stdio.h> 2 #include <math.h> 3 4 int main(void) 5 { 6 int a, b, i; 7 float s; 8 9 /* Pedir l´ımites inferior y superior. */ 10 do { 11 printf ("L´ımite inferior:"); scanf ("%d", &a); 12 if (a < 0) printf ("No puede ser negativon"); 13 } while (a < 0); 14 15 do { 16 printf ("L´ımite superior:"); scanf ("%d", &b); 17 if (b < a) printf ("No puede ser menor que %dn", a); 18 } while (b < a); 19 20 /* Calcular el sumatorio de la ra´ız cuadrada de i para i entre a y b. */ 21 s = 0.0; 22 for (i = a; i <= b; i++) s += sqrt(i); 23 24 /* Mostrar el resultado. */ 25 printf ("Sumatorio de ra´ıces de %d a %d: %fn", a, b, s); 26 27 return 0; 28 } Los bucles do-while no a˜naden potencia al lenguaje, pero s´ı lo dotan de mayor expresividad. Cualquier cosa que puedas hacer con bucles do-while, puedes hacerla tambi´en con s´olo bucles while y la ayuda de alguna sentencia condicional if, pero probablemente requerir´an mayor esfuerzo por tu parte. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 44 Escribe un programa que muestre un men´u en pantalla con dos opciones: ((saludar)) y ((salir)). El programa pedir´a al usuario una opci´on y, si es v´alida, ejecutar´a su acci´on asociada. Mientras no se seleccione la opci´on ((salir)), el men´u reaparecer´a y se solicitar´a nuevamente una opci´on. Implementa el programa haciendo uso ´unicamente de bucles do-while. · 45 Haz un programa que pida un n´umero entero de teclado distinto de 1. A continuaci´on, el programa generar´a una secuencia de n´umeros enteros cuyo primer n´umero es el que hemos le´ıdo y que sigue estas reglas: si el ´ultimo n´umero es par, el siguiente resulta de dividir a ´este por la mitad; si el ´ultimo n´umero es impar, el siguiente resulta de multiplicarlo por 3 y a˜nadirle 1. Todos los n´umeros se ir´an mostrando por pantalla conforme se vayan generando. El proceso se repetir´a hasta que el n´umero generado sea igual a 1. Utiliza un bucle do-while. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . El bucle for El bucle for de Python existe en C, pero con importantes diferencias. for (inicializaci´on; condici´on; incremento) { sentencias } Introducci´on a la Programaci´on con C 55
  • 62. 1.17 Estructuras de control Comparaciones y asignaciones Un error frecuente es sustituir el operador de comparaci´on de igualdad por el de asignaci´on en una estructura if o while. Analiza este par de sentencias: a = 0 if (a = 0) { // Lo que escribi´o... ? bien o mal? ... } Parece que la condici´on del if se eval´ua a cierto, pero no es as´ı: la ((comparaci´on)) es, en realidad, una asignaci´on. El resultado es que a recibe el valor 0 y que ese 0, devuelto por el operador de asignaci´on, se considera la representaci´on del valor ((falso)). Lo correcto hubiera sido: a = 0 if (a == 0) { // Lo que quer´ıa escribir. ... } Aunque esta construcci´on es perfectamente v´alida, provoca la emisi´on de un mensaje de error en muchos compiladores, pues suele ser fruto de un error. Los programadores m´as disciplinados evitan cometer este error escribiendo siempre la variable en la parte derecha: a = 0 if (0 == a) { // Correcto. ... } De ese modo, si se confunden y usan = en lugar de ==, se habr´a escrito una expresi´on incorrecta y el compilador detendr´a el proceso de traducci´on a c´odigo de m´aquina: a = 0 if (0 = a) { // Mal: error detectable por el compilador. ... } Los par´entesis de la primera l´ınea son obligatorios. F´ıjate, adem´as, en que los tres elementos entre par´entesis se separan con puntos y comas. El bucle for presenta tres componentes. Es equivalente a este fragmento de c´odigo: inicializaci´on; while (condici´on) { sentencias incremento; } Una forma habitual de utilizar el bucle for es la que se muestra en este ejemplo, que imprime por pantalla los n´umeros del 0 al 9 y en el que suponemos que i es de tipo int: for (i = 0; i < 10; i++) { printf ("%dn", i); } Es equivalente, como dec´ıamos, a este otro fragmento de programa: i = 0; while (i < 10) { printf ("%dn", i); i++; } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 46 Implementa el programa de c´alculo de xn (para x y n entero) con un bucle for. 56 Introducci´on a la Programaci´on con C
  • 63. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C · 47 Implementa un programa que dado un n´umero de tipo int, le´ıdo por teclado, se asegure de que s´olo contiene ceros y unos y muestre su valor en pantalla si lo interpretamos como un n´umero binario. Si el usuario introduce, por ejemplo, el n´umero 1101, el programa mostrar´a el valor 13. Caso de que el usuario introduzca un n´umero formado por n´umeros de valor diferente, indica al usuario que no puedes proporcionar el valor de su interpretaci´on como n´umero binario. · 48 Haz un programa que solicite un n´umero entero y muestre su factorial. Utiliza un entero de tipo long long para el resultado. Debes usar un bucle for. · 49 El n´umero de combinaciones de n elementos tomados de m en m es: Cm n = n m = n! (n − m)! m! . Dise˜na un programa que pida el valor de n y m y calcule Cm n . (Ten en cuenta que n ha de ser mayor o igual que m.) (Puedes comprobar la validez de tu programa introduciendo los valores n = 15 y m = 10: el resultado es 3003.) · 50 ¿Qu´e muestra por pantalla este programa? desplazamientos.c desplazamientos.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a = 127, b = 1024, c, i; 6 7 c = a ^ b; 8 9 printf ("%dn", c); 10 11 a = 2147483647; 12 for (i = 0; i < 8*sizeof(a); i++) { 13 printf ("%d", ((c & a) != 0) ? 1 : 0); 14 a >>= 1; 15 } 16 printf ("n"); 17 18 a = 1; 19 for (i = 0; i < 8*sizeof(a); i++) { 20 if ((c & a) != 0) c >>= 1; 21 else c <<= 1; 22 a <<= 1; 23 } 24 25 a = 2147483647; 26 for (i = 0; i < 8*sizeof(a); i++) { 27 printf ("%d", ((c & a) != 0) ? 1 : 0); 28 a >>= 1; 29 } 30 printf ("n"); 31 return 0; 32 } · 51 Cuando no era corriente el uso de terminales gr´aficos de alta resoluci´on era com´un representar gr´aficas de funciones con el terminal de caracteres. Por ejemplo, un periodo de la funci´on seno tiene este aspecto al representarse en un terminal de caracteres (cada punto es un asterisco): * * * * * * * * * * Introducci´on a la Programaci´on con C 57
  • 64. 1.17 Estructuras de control * * * * * * * * * * * * * * Haz un programa C que muestre la funci´on seno utilizando un bucle que recorre el periodo 2π en 24 pasos (es decir, represent´andolo con 24 l´ıneas). · 52 Modifica el programa para que muestre las funciones seno (con asteriscos) y coseno (con sumas) simult´aneamente. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Variables de bucle de usar y tirar C99 ha copiado una buena idea de C++: permitir que las variables de bucle se definan all´ı donde se usan y dejen de existir cuando el bucle termina. F´ıjate en este programa: for con variable.c for con variable.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a = 1; 6 7 for (int i = 0; i < 32; i++) { 8 printf ("2**%2d = %10un", i, a); 9 a <<= 1; 10 } 11 12 return 0; 13 } La variable i, el ´ındice del bucle, se declara en la mism´ısima zona de inicializaci´on del bucle. La variable i s´olo existe en el ´ambito del bucle, que es donde se usa. Hacer un bucle que recorra, por ejemplo, los n´umeros pares entre 0 y 10 es sencillo: basta sustituir el modo en que se incrementa la variable ´ındice: for (i = 0; i < 10; i = i + 2) { printf ("%dn", i); } aunque la forma habitual de expresar el incremento de i es esta otra: for (i = 0; i < 10; i += 2) { printf ("%dn", i); } Un bucle que vaya de 10 a 1 en orden inverso presenta este aspecto: for (i = 10; i > 0; i--) { printf ("%dn", i); } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 53 Dise˜na un programa C que muestre el valor de 2n para todo n entre 0 y un valor entero proporcionado por teclado. 58 Introducci´on a la Programaci´on con C
  • 65. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C · 54 Haz un programa que pida al usuario una cantidad de euros, una tasa de inter´es y un n´umero de a˜nos y muestre por pantalla en cu´anto se habr´a convertido el capital inicial transcurridos esos a˜nos si cada a˜no se aplica la tasa de inter´es introducida. Recuerda que un capital C a un inter´es del x por cien durante n a˜nos se convierte en C · (1 + x/100)n . (Prueba tu programa sabiendo que 10 000 euros al 4.5% de inter´es anual se convierten en 24 117.14 euros al cabo de 20 a˜nos.) · 55 Un vector en un espacio tridimensional es una tripleta de valores reales (x, y, z). Desea- mos confeccionar un programa que permita operar con dos vectores. El usuario ver´a en pantalla un men´u con las siguientes opciones: 1) Introducir el primer vector 2) Introducir el segundo vector 3) Calcular la suma 4) Calcular la diferencia 5) Calcular el producto vectorial 6) Calcular el producto escalar 7) Calcular el ´angulo (en grados) entre ellos 8) Calcular la longitud 9) Finalizar Tras la ejecuci´on de cada una de las acciones del men´u ´este reaparecer´a en pantalla, a menos que la opci´on escogida sea la n´umero 9. Si el usuario escoge una opci´on diferente, el programa advertir´a al usuario de su error y el men´u reaparecer´a. Las opciones 4 y 5 pueden proporcionar resultados distintos en funci´on del orden de los operandos, as´ı que, si se escoge cualquiera de ellas, aparecer´a un nuevo men´u que permita seleccionar el orden de los operandos. Por ejemplo, la opci´on 4 mostrar´a el siguiente men´u: 1) Primer vector menos segundo vector 2) Segundo vector menos primer vector Nuevamente, si el usuario se equivoca, se le advertir´a del error y se le permitir´a corregirlo. La opci´on 8 del men´u principal conducir´a tambi´en a un submen´u para que el usuario decida sobre qu´e vector se aplica el c´alculo de longitud. Puede que necesites que te refresquemos la memoria sobre los c´alculos a realizar. Quiz´a la siguiente tabla te sea de ayuda: Operaci´on C´alculo Suma: (x1, y1, z1) + (x2, y2, z2) (x1 + x2, y1 + y2, z1 + z2) Diferencia: (x1, y1, z1) − (x2, y2, z2) (x1 − x2, y1 − y2, z1 − z2) Producto escalar: (x1, y1, z1) · (x2, y2, z2) x1x2 + y1y2 + z1z2 Producto vectorial: (x1, y1, z1) × (x2, y2, z2) (y1z2 − z1y2, z1x2 − x1z2, x1y2 − y1x2) ´Angulo entre (x1, y1, z1) y (x2, y2, z2) 180 π · arccos x1x2 + y1y2 + z1z2 x2 1 + y2 1 + z2 1 x2 2 + y2 2 + z2 2 Longitud de (x, y, z) x2 + y2 + z2 Ten en cuenta que tu programa debe contemplar toda posible situaci´on excepcional: divi- siones por cero, ra´ıces con argumento negativo, etc.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.17.3. Sentencias para alterar el flujo iterativo La sentencia break tambi´en est´a disponible en C. De hecho, ya hemos visto una aplicaci´on suya en la estructura de control switch. Con ella puedes, adem´as, abortar al instante la ejecuci´on de un bucle cualquiera (while, do-while o for). Otra sentencia de C que puede resultar ´util es continue. Esta sentencia finaliza la iteraci´on actual, pero no aborta la ejecuci´on del bucle. Introducci´on a la Programaci´on con C 59
  • 66. 1.17 Estructuras de control Por ejemplo, cuando en un bucle while se ejecuta continue, la siguiente sentencia a ejecutar es la condici´on del bucle; si ´esta se cumple, se ejecutar´a una nueva iteraci´on del bucle. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 56 ¿Qu´e muestra por pantalla este programa? continue.c continue.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int i; 6 7 i = 0; 8 while (i < 10) { 9 if (i % 2 == 0) { 10 i++; 11 continue; 12 } 13 printf ("%dn", i); 14 i++; 15 } 16 17 for (i = 0; i < 10; i++) { 18 if (i % 2 != 0) 19 continue; 20 printf ("%dn", i); 21 } 22 return 0; 23 } · 57 Traduce a C este programa Python. 1 car = raw_input(’Dame un car´acter: ’) 2 if "a" <= car.lower() <= "z" or car == " ": 3 print "Este car´acter es v´alido en un identificador en Python." 4 else: 5 if not (car < "0" or "9" < car): 6 print "Un d´ıgito es v´alido en un identificador en Python,", 7 print "siempre que no sea el primer car´acter." 8 else: 9 print "Car´acter no v´alido para formar un identificador en Python." · 58 Traduce a C este programa Python. 1 from math import pi 2 radio = float(raw_input(’Dame el radio de un c´ırculo: ’)) 3 opcion = ’’ 4 while opcion != ’a’ and opcion != ’b’ and opcion != ’c’: 5 print ’Escoge una opci´on: ’ 6 print ’a) Calcular el di´ametro.’ 7 print ’b) Calcular el per´ımetro.’ 8 print ’c) Calcular el ´area.’ 9 opcion = raw_input(’Teclea a, b o c y pulsa el retorno de carro: ’) 10 if opcion == ’a’: 11 diametro = 2 * radio 12 print ’El di´ametro es’, diametro 13 elif opcion == ’b’: 14 perimetro = 2 * pi * radio 15 print ’El per´ımetro es’, perimetro 16 elif opcion == ’c’: 17 area = pi * radio ** 2 18 print ’El ´area es’, area 19 else: 20 print ’S´olo hay tres opciones: a, b o c. T´u has tecleado’, opcion 60 Introducci´on a la Programaci´on con C
  • 67. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 1 Introducci´on a C · 59 Traduce a C este programa Python. 1 anyo = int(raw_input(’Dame un a~no: ’)) 2 if anyo % 4 == 0 and (anyo % 100 != 0 or anyo % 400 == 0): 3 print ’El a~no’, anyo, ’es bisiesto.’ 4 else: 5 print ’El a~no’, anyo, ’no es bisiesto.’ · 60 Traduce a C este programa Python. 1 limite = int(raw_input(’Dame un n´umero: ’)) 2 3 for num in range(1, limite+1): 4 creo_que_es_primo = 1 5 for divisor in range(2, num): 6 if num % divisor == 0: 7 creo_que_es_primo = 0 8 break 9 if creo_que_es_primo == 1: 10 print num · 61 Escribe un programa que solicite dos enteros n y m asegur´andose de que m sea mayor o igual que n. A continuaci´on, muestra por pantalla el valor de m i=n 1/i. · 62 Escribe un programa que solicite un n´umero entero y muestre todos los n´umeros primos entre 1 y dicho n´umero. · 63 Haz un programa que calcule el m´aximo com´un divisor (mcd) de dos enteros positivos. El mcd es el n´umero m´as grande que divide exactamente a ambos n´umeros. · 64 Haz un programa que calcule el m´aximo com´un divisor (mcd) de tres enteros positivos. · 65 Haz un programa que vaya leyendo n´umeros y mostr´andolos por pantalla hasta que el usuario introduzca un n´umero negativo. En ese momento, el programa acabar´a mostrando un mensaje de despedida. · 66 Haz un programa que vaya leyendo n´umeros hasta que el usuario introduzca un n´umero negativo. En ese momento, el programa mostrar´a por pantalla el n´umero mayor de cuantos ha visto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Introducci´on a la Programaci´on con C 61
  • 68. 1.17 Estructuras de control 62 Introducci´on a la Programaci´on con C
  • 69. Cap´ıtulo 2 Estructuras de datos en C: vectores est´aticos y registros —Me llamo Alicia, Majestad —dijo Alicia con mucha educaci´on; pero a˜nadi´o para sus adentros: ((¡Vaya!, en realidad no son m´as que un mazo de cartas. ¡No tengo por qu´e tenerles miedo!)). Lewis Carroll, Alicia en el Pa´ıs de las Maravillas. En este cap´ıtulo vamos a estudiar algunas estructuras que agrupan varios datos, pero cuyo tama˜no resulta conocido al compilar el programa y no sufre modificaci´on alguna durante su ejecuci´on. Empezaremos estudiando los vectores, estructuras que se pueden asimilar a las listas Python. En C, las cadenas son un tipo particular de vector. Manejar cadenas en C resulta m´as complejo y delicado que manejarlas en Python. Como contrapartida, es m´as f´acil definir en C vectores multidimensionales (como las matrices) que en Python. En este cap´ıtulo nos ocuparemos tambi´en de ellos. Estudiaremos adem´as los registros en C, que permiten definir nuevos tipos como agrupaciones de datos de tipos no necesariamente id´enticos. Los registros de C son conceptualmente id´enticos a los que estudiamos en Python. 2.1. Vectores est´aticos Un vector (en ingl´es, ((array))) es una secuencia de valores a los que podemos acceder mediante ´ındices que indican sus respectivas posiciones. Los vectores pueden asimilarse a las listas Python, pero con una limitaci´on fundamental: todos los elementos del vector han de tener el mismo tipo. Podemos definir vectores de enteros, vectores de flotantes, etc., pero no podemos definir vectores que, por ejemplo, contengan a la vez enteros y flotantes. El tipo de los elementos de un vector se indica en la declaraci´on del vector. C nos permite trabajar con vectores est´aticos y din´amicos. En este cap´ıtulo nos ocupamos ´unicamente de los denominados vectores est´aticos, que son aquellos que tienen tama˜no fijo y conocido en tiempo de compilaci´on. Es decir, el n´umero de elementos del vector no puede depender de datos que suministra el usuario: se debe hacer expl´ıcito mediante una expresi´on que podamos evaluar examinando ´unicamente el texto del programa. 2.1.1. Declaraci´on de vectores Un vector a de 10 enteros de tipo int se declara as´ı: int a[10]; El vector a comprende los elementos a[0], a[1], a[2], . . . , a[9], todos de tipo int. Al igual que con las listas Python, los ´ındices de los vectores C empiezan en cero. En una misma l´ınea puedes declarar m´as de un vector, siempre que todos compartan el mismo tipo de datos para sus componentes. Por ejemplo, en esta l´ınea se declaran dos vectores de float, uno con 20 componentes y otro con 100: Introducci´on a la Programaci´on con C 63
  • 70. 2.1 Vectores est´aticos Sin cortes Los vectores C son mucho m´as limitados que las listas Python. A los problemas relacionados con el tama˜no fijo de los vectores o la homogeneidad en el tipo de sus elementos se une una incomodidad derivada de la falta de operadores a los que nos hemos acostumbrado como programadores Python. El operador de corte, por ejemplo, no existe en C. Cuando en Python dese´abamos extraer una copia de los elementos entre i y j de un vector a escrib´ıamos a[i:j+1]. En C no hay operador de corte. . . ni operador de concatenaci´on o repetici´on, ni sentencias de borrado de elementos, ni se entienden como accesos desde el final los´ındices negativos, ni hay operador de pertenencia, etc. Echaremos de menos muchas de las facilidades propias de Python. float a[20], b[100]; Tambi´en es posible mezclar declaraciones de vectores y escalares en una misma l´ınea. En este ejemplo se declaran las variables a y c como vectores de 80 caracteres y la variable b como escalar de tipo car´acter: char a[80], b, c[80]; Se considera mal estilo declarar la talla de los vectores con literales de entero. Es preferible utilizar alg´un identificador para la talla, pero teniendo en cuenta que ´este debe corresponder a una constante: #define TALLA 80 ... char a[TALLA]; Esta otra declaraci´on es incorrecta, pues usa una variable para definir la talla del vector1 : int talla = 80; ... char a[talla]; // ! No siempre es v´alido! Puede que consideres v´alida esta otra declaraci´on que prescinde de constantes definidas con define y usa constantes declaradas con const, pero no es as´ı: const int talla = 80; ... char a[talla]; // ! No siempre es v´alido! Una variable const es una variable en toda regla, aunque de ((s´olo lectura)). 2.1.2. Inicializaci´on de los vectores Una vez creado un vector, sus elementos presentan valores arbitrarios. Es un error suponer que los valores del vector son nulos tras su creaci´on. Si no lo crees, f´ıjate en este programa: sin inicializar.c sin inicializar.c 1 #include <stdio.h> 2 3 #define TALLA 5 4 5 int main(void) 6 { 7 int i, a[TALLA]; 8 9 for (i = 0; i < TALLA; i++) 10 printf ("%dn", a[i]); 11 return 0; 12 } 1Como siempre, hay excepciones: C99 permite declarar la talla de un vector con una expresi´on cuyo valor s´olo se conoce en tiempo de ejecuci´on, pero s´olo si el vector es una variable local a una funci´on. Para evitar confusiones, no haremos uso de esa caracter´ıstica en este cap´ıtulo y lo consideraremos incorrecto. 64 Introducci´on a la Programaci´on con C
  • 71. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros Observa que el acceso a elementos del vector sigue la misma notaci´on de Python: usamos el identificador del vector seguido del ´ındice encerrado entre corchetes. En una ejecuci´on del programa obtuvimos este resultado en pantalla (es probable que obtengas resultados diferentes si repites el experimento): 1073909760 1075061012 1205 1074091790 1073941880 Evidentemente, no son cinco ceros. Podemos inicializar todos los valores de un vector a cero con un bucle for: inicializados a cero.c inicializados a cero.c 1 #include <stdio.h> 2 3 #define TALLA 10 4 5 int main(void) 6 { 7 int i, a[TALLA]; 8 9 for (i = 0; i < TALLA; i++) 10 a[i] = 0; 11 12 for (i = 0; i < TALLA; i++) 13 printf ("%dn", a[i]); 14 15 return 0; 16 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 67 Declara e inicializa un vector de 100 elementos de modo que los componentes de ´ındice par valgan 0 y los de ´ındice impar valgan 1. · 68 Escribe un programa C que almacene en un vector los 50 primeros n´umeros de Fibonacci. Una vez calculados, el programa los mostrar´a por pantalla en orden inverso. · 69 Escribe un programa C que almacene en un vector los 50 primeros n´umeros de Fibonacci. Una vez calculados, el programa pedir´a al usuario que introduzca un n´umero y dir´a si es o no es uno de los 50 primeros n´umeros de Fibonacci. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Hay una forma alternativa de inicializar vectores. En este fragmento se definen e inicializan dos vectores, uno con todos sus elementos a 0 y otro con una secuencia ascendente de n´umeros: 1 #define TALLA 5 2 ... 3 int a[TALLA] = {0, 0, 0, 0, 0}; 4 int b[TALLA] = {1, 2, 3, 4, 5}; Ten en cuenta que, al declarar e inicializar simult´aneamente un vector, debes indicar expl´ıci- tamente los valores del vector y, por tanto, esta aproximaci´on s´olo es factible para la inicializa- ci´on de unos pocos valores. 2.1.3. Un programa de ejemplo: la criba de Erat´ostenes Vamos a ilustrar lo aprendido desarrollando un sencillo programa que calcule y muestre los n´umeros primos menores que N, para un valor de N fijo y determinado en el propio programa. Usaremos un m´etodo denominado la criba de Erat´ostenes, uno de los algoritmos m´as antiguos y que debemos a un astr´onomo, ge´ografo, matem´atico y fil´osofo de la antigua Grecia. El m´etodo utiliza un vector de N valores booleanos (unos o ceros). Si la celda de ´ındice i contiene el valor 1, Introducci´on a la Programaci´on con C 65
  • 72. 2.1 Vectores est´aticos Cuesti´on de estilo: ¿constantes o literales al declarar la talla de un vector? ¿Por qu´e se prefiere declarar el tama˜no de los vectores con constantes en lugar de con literales de entero? Porque la talla del vector puede aparecer en diferentes puntos del programa y es posible que alg´un d´ıa hayamos de modificar el programa para trabajar con un vector de talla diferente. En tal caso, nos ver´ıamos obligados a editar muchas l´ıneas diferentes del programa (puede que decenas o cientos). Bastar´ıa con que olvid´asemos modificar una o con que modific´asemos una de m´as para que el programa fuera err´oneo. F´ıjate en este programa C: 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int i, a[10], b[10]; 6 7 for (i = 0; i < 10; i++) 8 a[i] = 0; 9 for (i = 0; i < 10; i++) 10 b[i] = 0; 11 for (i = 0; i < 10; i++) 12 printf ("%dn", a[i]); 13 for (i = 0; i < 10; i++) 14 printf ("%dn", b[i]); 15 return 0; 16 } Las tallas de los vectores a y b aparecen en seis lugares diferentes: en sus declaraciones, en los bucles que los inicializan y en los que se imprimen. Imagina que deseas modificar el programa para que a pase a tener 20 enteros: tendr´as que modificar s´olo tres de esos dieces. Ello te obliga a leer el programa detenidamente y, cada vez que encuentres un diez, pararte a pensar si ese diez en particular corresponde o no a la talla de a. Innecesariamente complicado. Estudia esta alternativa: 1 #include <stdio.h> 2 3 #define TALLA_A 10 4 #define TALLA_B 10 5 6 int main(void) 7 { 8 int i, a[TALLA_A], b[TALLA_B]; 9 10 for (i = 0; i < TALLA_A; i++) 11 a[i] = 0; 12 for (i = 0; i < TALLA_B; i++) 13 b[i] = 0; 14 for (i = 0; i < TALLA_A; i++) 15 printf ("%dn", a[i]); 16 for (i = 0; i < TALLA_B; i++) 17 printf ("%dn", b[i]); 18 return 0; 19 } Si ahora necesitas modificar a para que tenga 20 elementos, basta con que edites la l´ınea 3 sustituyendo el 10 por un 20. Mucho m´as r´apido y con mayor garant´ıa de no cometer errores. ¿Por qu´e en Python no nos preocup´o esta cuesti´on? Recuerda que en Python no hab´ıa declaraci´on de variables, que las listas pod´ıan modificar su longitud durante la ejecuci´on de los programas y que pod´ıas consultar la longitud de cualquier secuencia de valores con la funci´on predefinida len. Python ofrece mayores facilidades al programador, pero a un doble precio: la menor velocidad de ejecuci´on y el mayor consumo de memoria. 66 Introducci´on a la Programaci´on con C
  • 73. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros Omisi´on de talla en declaraciones con inicializaci´on y otro modo de inicializar Tambi´en puedes declarar e inicializar vectores as´ı: int a[] = {0, 0, 0, 0, 0}; int b[] = {1, 2, 3, 4, 5}; El compilador deduce que la talla del vector es 5, es decir, el n´umero de valores que aparecen a la derecha del igual. Te recomendamos que, ahora que est´as aprendiendo, no uses esta forma de declarar vectores: siempre que puedas, opta por una que haga expl´ıcito el tama˜no del vector. En C99 es posible inicializar s´olo algunos valores del vector. La sintaxis es un poco enrevesada. Aqu´ı tienes un ejemplo en el que s´olo inicializamos el primer y ´ultimo elementos de un vector de talla 10: int a[] = {[0] = 0, [9] = 0}; consideramos que i es primo, y si no, que no lo es. Inicialmente, todas las celdas excepto la de ´ındice 0 valen 1. Entonces ((tachamos)) (ponemos un 0 en) las celdas cuyo ´ındice es m´ultiplo de 2. Acto seguido se busca la siguiente casilla que contiene un 1 y se procede a tachar todas las casillas cuyo ´ındice es m´ultiplo del ´ındice de esta casilla. Y as´ı sucesivamente. Cuando se ha recorrido completamente el vector, las casillas cuyo ´ındice es primo contienen un 1. Vamos con una primera versi´on del programa: eratostenes.c eratostenes.c 1 #include <stdio.h> 2 3 #define N 100 4 5 int main(void) 6 { 7 int criba[N], i, j; 8 9 /* Inicializaci´on */ 10 criba[0] = 0; 11 for (i=1; i<N; i++) 12 criba[i] = 1; 13 14 /* Criba de Erat´ostenes */ 15 for (i=2; i<N; i++) 16 if (criba[i]) 17 for (j=2; i*j<N; j++) 18 criba[i*j] = 0; 19 20 /* Mostrar los resultados */ 21 for (i=0; i<N; i++) 22 if (criba[i]) 23 printf ("%dn", i); 24 25 return 0; 26 } Observa que hemos tenido que decidir qu´e valor toma N, pues el vector criba debe tener un tama˜no conocido en el momento en el que se compila el programa. Si deseamos conocer los, digamos, primos menores que 200, tenemos que modificar la l´ınea 3. Mejoremos el programa. ¿Es necesario utilizar 4 bytes para almacenar un 0 o un 1? Estamos malgastando memoria. Esta otra versi´on reduce a una cuarta parte el tama˜no del vector criba: eratostenes 1.c eratostenes.c 1 #include <stdio.h> 2 Introducci´on a la Programaci´on con C 67
  • 74. 2.1 Vectores est´aticos 3 #define N 100 4 5 int main(void) 6 { 7 char criba[N]; 8 int i, j; 9 10 /* Inicializaci´on */ 11 criba[0] = 0; 12 for (i=1; i<N; i++) 13 criba[i] = 1; 14 15 /* Criba de Erat´ostenes */ 16 for (i=2; i<N; i++) 17 if (criba[i]) 18 for (j=2; i*j<N; j++) 19 criba[i*j] = 0; 20 21 /* Mostrar los resultados */ 22 for (i=0; i<N; i++) 23 if (criba[i]) 24 printf ("%dn", i); 25 26 return 0; 27 } Mejor as´ı. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 70 Puedes ahorrar tiempo de ejecuci´on haciendo que i tome valores entre 2 y la ra´ız cuadrada de N. Modifica el programa y comprueba que obtienes el mismo resultado. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1.4. Otro programa de ejemplo: estad´ısticas Queremos efectuar estad´ısticas con una serie de valores (las edades de 15 personas), as´ı que vamos a dise˜nar un programa que nos ayude. En una primera versi´on, solicitaremos las edades de todas las personas y, a continuaci´on, calcularemos y mostraremos por pantalla la edad media, la desviaci´on t´ıpica, la moda y la mediana. Las f´ormulas para el c´alculo de la media y la desviaci´on t´ıpica de n elementos son: ¯x = n i=1 xi n , σ = n i=1(xi − ¯x)2 n , donde xi es la edad del individuo n´umero i.2 La moda es la edad que m´as veces aparece (si dos o m´as edades aparecen muchas veces con la m´axima frecuencia, asumiremos que una cualquiera de ellas es la moda). La mediana es la edad tal que el 50% de las edades son inferiores o iguales a ella y el restante 50% son mayores o iguales. Empezamos por la declaraci´on del vector que albergar´a las 15 edades y por leer los datos: edades.c edades.c 1 #include <stdio.h> 2 3 #define PERSONAS 15 4 5 int main(void) 6 { 7 int edad[PERSONAS], i; 8 2Hay una definici´on alternativa de la desviaci´on t´ıpica en la que el denominador de la fracci´on es n − 1. 68 Introducci´on a la Programaci´on con C
  • 75. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros Optimiza, pero no te pases C permite optimizar mucho los programas y hacer que estos consuman la menor memoria posible o que se ejecuten a mucha velocidad gracias a una adecuada selecci´on de operaciones. En el programa de la criba de Erat´ostenes, por ejemplo, a´un podemos reducir m´as el consumo de memoria: para representar un 1 o un 0 basta un solo bit. Como en un char caben 8 bits, podemos proponer esta otra soluci´on: eratostenes bit.c eratostenes bit.c 1 #include <stdio.h> 2 #include <math.h> 3 4 #define N 100 5 6 int main(void) 7 { 8 char criba[N/8+1]; // Ocupa unas 8 veces menos que la versi´on anterior. 9 int i, j; 10 11 /* Inicializaci´on */ 12 criba[0] = 254; // Pone todos los bits a 1 excepto el primero. 13 for (i=1; i<=N/8; i++) 14 criba[i] = 255; // Pone todos los bits a 1. 15 16 /* Criba de Erat´ostenes */ 17 for (i=2; i<N; i++) 18 if (criba[i/8] & (1 << (i%8))) // Pregunta si el bit en posici´on i vale 1. 19 for (j=2; i*j<N; j++) 20 criba[i*j/8] &= ~(1 << ((i*j) % 8)); // Pone a 0 el bit en posici´on i*j. 21 22 /* Mostrar los resultados */ 23 for (i=0; i<N; i++) 24 if (criba[i/8] & 1 << (i%8)) 25 printf ("%dn", i); 26 27 return 0; 28 } ¡Buf! La legibilidad deja mucho que desear. Y no s´olo eso: consultar si un determinado bit vale 1 y fijar un determinado bit a 0 resultan ser operaciones m´as costosas que consultar si el valor de un char es 1 o, respectivamente, fijar el valor de un char a 0, pues debes hacerlo mediante operaciones de divisi´on entera, resto de divisi´on entera, desplazamiento, negaci´on de bits y el operador &. ¿Vale la pena reducir la memoria a una octava parte si, a cambio, el programa pierde legibilidad y, adem´as, resulta m´as lento? No hay una respuesta definitiva a esta pregunta. La ´unica respuesta es: depende. En seg´un qu´e aplicaciones, puede resultar necesario, en otras no. Lo que no debes hacer, al menos de momento, es obsesionarte con la optimizaci´on y complicar innecesariamente tus programas. 9 /* Lectura de edades */ 10 for (i=0; i<PERSONAS; i++) { 11 printf ("Por favor, introduce edad de la persona n´umero %d: ", i+1); 12 scanf ("%d", &edad[i]); 13 } 14 15 return 0; 16 } Vale la pena que te detengas a observar c´omo indicamos a scanf que lea la celda de ´ındice i en el vector edad: usamos el operador & delante de la expresi´on edad[i]. Es lo que cab´ıa esperar: edad[i] es un escalar de tipo int, y ya sabes que scanf espera su direcci´on de memoria. Pasamos ahora a calcular la edad media y la desviaci´on t´ıpica (no te ha de suponer dificultad alguna con la experiencia adquirida al aprender Python): Introducci´on a la Programaci´on con C 69
  • 76. 2.1 Vectores est´aticos edades 1.c edades.c 1 #include <stdio.h> 2 #include <math.h> 3 4 #define PERSONAS 15 5 6 int main(void) 7 { 8 int edad[PERSONAS], i, suma_edad ; 9 float suma_desviacion, media, desviacion ; 10 11 /* Lectura de edades */ 12 for (i=0; i<PERSONAS; i++) { 13 printf ("Por favor, introduce edad de la persona n´umero %d: ", i+1); 14 scanf ("%d", &edad[i]); 15 } 16 17 /* C´alculo de la media */ 18 suma_edad = 0; 19 for (i=0; i<PERSONAS; i++) 20 suma_edad += edad[i]; 21 media = suma_edad / (float) PERSONAS; 22 23 /* C´alculo de la desviacion t´ıpica */ 24 suma_desviacion = 0.0; 25 for (i=0; i<PERSONAS; i++) 26 suma_desviacion += (edad[i] - media) * (edad[i] - media); 27 desviacion = sqrt( suma_desviacion / PERSONAS ); 28 29 /* Impresi´on de resultados */ 30 printf ("Edad media : %fn", media); 31 printf ("Desv. t´ıpica: %fn", desviacion); 32 33 return 0; 34 } El c´alculo de la moda (la edad m´as frecuente) resulta m´as problem´atica. ¿C´omo abordar el c´alculo? Vamos a presentar dos versiones diferentes. Empezamos por una que consume dema- siada memoria. Dado que trabajamos con edades, podemos asumir que ninguna edad iguala o supera los 150 a˜nos. Podemos crear un vector con 150 contadores, uno para cada posible edad: edades 2.c edades.c 1 #include <stdio.h> 2 #include <math.h> 3 4 #define PERSONAS 15 5 #define MAX_EDAD 150 6 7 int main(void) 8 { 9 int edad[PERSONAS], i, suma_edad; 10 float suma_desviacion, media, desviacion; 11 int contador[MAX_EDAD], frecuencia, moda; 12 13 /* Lectura de edades */ 14 for (i=0; i<PERSONAS; i++) { 15 printf ("Por favor, introduce edad de la persona n´umero %d: ", i+1); 16 scanf ("%d", &edad[i]); 17 } 18 19 /* C´alculo de la media */ 20 suma_edad = 0; 21 for (i=0; i<PERSONAS; i++) 22 suma_edad += edad[i]; 70 Introducci´on a la Programaci´on con C
  • 77. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros 23 media = suma_edad / (float) PERSONAS; 24 25 /* C´alculo de la desviacion t´ıpica */ 26 suma_desviacion = 0.0; 27 for (i=0; i<PERSONAS; i++) 28 suma_desviacion += (edad[i] - media) * (edad[i] - media); 29 desviacion = sqrt( suma_desviacion / PERSONAS ); 30 31 /* C´alculo de la moda */ 32 for (i=0; i<MAX_EDAD; i++) // Inicializaci´on de los contadores. 33 contador[i] = 0; 34 for (i=0; i<PERSONAS; i++) 35 contador[edad[i]]++; // Incrementamos el contador asociado a edad[i]. 36 moda = -1; 37 frecuencia = 0; 38 for (i=0; i<MAX_EDAD; i++) // B´usqueda de la moda (edad con mayor valor del contador). 39 if (contador[i] > frecuencia) { 40 frecuencia = contador[i]; 41 moda = i; 42 } 43 44 /* Impresi´on de resultados */ 45 printf ("Edad media : %fn", media); 46 printf ("Desv. t´ıpica: %fn", desviacion); 47 printf ("Moda : %dn", moda); 48 49 return 0; 50 } Esta soluci´on consume un vector de 150 elementos enteros cuando no es estrictamente ne- cesario. Otra posibilidad pasa por ordenar el vector de edades y contar la longitud de cada secuencia de edades iguales. La edad cuya secuencia sea m´as larga es la moda: edades 3.c edades.c 1 #include <stdio.h> 2 #include <math.h> 3 4 #define PERSONAS 15 5 6 int main(void) 7 { 8 int edad[PERSONAS], i, j, aux , suma_edad; 9 float suma_desviacion, media, desviacion; 10 int moda, frecuencia, frecuencia_moda ; 11 12 /* Lectura de edades */ 13 for (i=0; i<PERSONAS; i++) { 14 printf ("Por favor, introduce edad de la persona n´umero %d: ", i+1); 15 scanf ("%d", &edad[i]); 16 } 17 18 /* C´alculo de la media */ 19 suma_edad = 0; 20 for (i=0; i<PERSONAS; i++) 21 suma_edad += edad[i]; 22 media = suma_edad / (float) PERSONAS; 23 24 /* C´alculo de la desviacion t´ıpica */ 25 suma_desviacion = 0.0; 26 for (i=0; i<PERSONAS; i++) 27 suma_desviacion += (edad[i] - media) * (edad[i] - media); 28 desviacion = sqrt( suma_desviacion / PERSONAS ); 29 30 /* C´alculo de la moda */ Introducci´on a la Programaci´on con C 71
  • 78. 2.1 Vectores est´aticos 31 for (i=0; i<PERSONAS-1; i++) // Ordenaci´on mediante burbuja. 32 for (j=0; j<PERSONAS-i; j++) 33 if (edad[j] > edad[j+1]) { 34 aux = edad[j]; 35 edad[j] = edad[j+1]; 36 edad[j+1] = aux; 37 } 38 39 frecuencia = 0; 40 frecuencia_moda = 0; 41 moda = -1; 42 for (i=0; i<PERSONAS-1; i++) // B´usqueda de la serie de valores id´enticos m´as larga. 43 if (edad[i] == edad[i+1]) { 44 frecuencia++; 45 if (frecuencia > frecuencia_moda) { 46 frecuencia_moda = frecuencia; 47 moda = edad[i]; 48 } 49 } 50 else 51 frecuencia = 0; 52 53 /* Impresi´on de resultados */ 54 printf ("Edad media : %fn", media); 55 printf ("Desv. t´ıpica: %fn", desviacion); 56 printf ("Moda : %dn", moda); 57 58 return 0; 59 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 71 ¿Contiene en cada instante la variable frecuencia el verdadero valor de la frecuencia de aparici´on de un valor? Si no es as´ı, ¿qu´e contiene? ¿Afecta eso al c´alculo efectuado? ¿Por qu´e? · 72 Esta nueva versi´on del programa presenta la ventaja adicional de no fijar un l´ımite m´aximo a la edad de las personas. El programa resulta, as´ı, de aplicaci´on m´as general. ¿Son todo ventajas? ¿Ves alg´un aspecto negativo? Reflexiona sobre la velocidad de ejecuci´on del programa comparada con la del programa que consume m´as memoria. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . S´olo nos resta calcular la mediana. Mmmm. No hay que hacer nuevos c´alculos para conocer la mediana: gracias a que hemos ordenado el vector, la mediana es el valor que ocupa la posici´on central del vector, es decir, la edad de ´ındice PERSONAS/2. edades 4.c edades.c 1 #include <stdio.h> 2 #include <math.h> 3 4 #define PERSONAS 15 5 6 int main(void) 7 { 8 int edad[PERSONAS], i, j, aux, suma_edad; 9 float suma_desviacion, media, desviacion; 10 int moda, frecuencia, frecuencia_moda, mediana ; 11 12 /* Lectura de edades */ 13 for (i=0; i<PERSONAS; i++) { 14 printf ("Por favor, introduce edad de la persona n´umero %d: ", i+1); 15 scanf ("%d", &edad[i]); 16 } 17 18 /* C´alculo de la media */ 19 suma_edad = 0; 72 Introducci´on a la Programaci´on con C
  • 79. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros 20 for (i=0; i<PERSONAS; i++) 21 suma_edad += edad[i]; 22 media = suma_edad / (float) PERSONAS; 23 24 /* C´alculo de la desviacion t´ıpica */ 25 suma_desviacion = 0.0; 26 for (i=0; i<PERSONAS; i++) 27 suma_desviacion += (edad[i] - media) * (edad[i] - media); 28 desviacion = sqrt( suma_desviacion / PERSONAS ); 29 30 /* C´alculo de la moda */ 31 for (i=0; i<PERSONAS-1; i++) // Ordenaci´on mediante burbuja. 32 for (j=0; j<PERSONAS-i; j++) 33 if (edad[j] > edad[j+1]) { 34 aux = edad[j]; 35 edad[j] = edad[j+1]; 36 edad[j+1] = aux; 37 } 38 39 frecuencia = 0; 40 frecuencia_moda = 0; 41 moda = -1; 42 for (i=0; i<PERSONAS-1; i++) 43 if (edad[i] == edad[i+1]) 44 if (++frecuencia > frecuencia_moda) { // Ver ejercicio 73. 45 frecuencia_moda = frecuencia; 46 moda = edad[i]; 47 } 48 else 49 frecuencia = 0; 50 51 /* C´alculo de la mediana */ 52 mediana = edad[PERSONAS/2] 53 54 /* Impresi´on de resultados */ 55 printf ("Edad media : %fn", media); 56 printf ("Desv. t´ıpica: %fn", desviacion); 57 printf ("Moda : %dn", moda); 58 printf ("Mediana : %dn", mediana); 59 60 return 0; 61 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 73 F´ıjate en la l´ınea 44 del programa y comp´arala con las l´ıneas 44 y 45 de su versi´on anterior. ¿Es correcto ese cambio? ¿Lo ser´ıa este otro?: 44 if (frecuencia++ > frecuencia_moda) { . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Bueno, vamos a modificar ahora el programa para que el usuario introduzca cuantas edades desee hasta un m´aximo de 20. Cuando se introduzca un valor negativo para la edad, entende- remos que ha finalizado la introducci´on de datos. edades.c 1 #include <stdio.h> 2 #include <math.h> 3 4 #define PERSONAS 20 5 6 int main(void) 7 { 8 int edad[PERSONAS], i, j, aux, suma_edad; 9 float suma_desviacion, media, desviacion; Introducci´on a la Programaci´on con C 73
  • 80. 2.1 Vectores est´aticos 10 int moda, frecuencia, frecuencia_moda, mediana; 11 12 /* Lectura de edades */ 13 for (i=0; i<PERSONAS; i++) { 14 printf ("Introduce edad de la persona %d (si es negativa, acaba): ", i+1); 15 scanf ("%d", &edad[i]); 16 if (edad[i] < 0) 17 break; 18 } 19 20 ... 21 22 return 0; 23 } Mmmm. Hay un problema: si no damos 20 edades, el vector presentar´a toda una serie de valores sin inicializar y, por tanto, con valores arbitrarios. Ser´ıa un grave error tomar esos valores por edades introducidas por el usuario. Una buena idea consiste en utilizar una variable entera que nos diga en todo momento cu´antos valores introdujo realmente el usuario en el vector edad: edades 5.c edades.c 1 #include <stdio.h> 2 #include <math.h> 3 4 #define MAX_PERSONAS 20 5 6 int main(void) 7 { 8 int edad[MAX_PERSONAS], personas , i, j, aux, suma_edad; 9 float suma_desviacion, media, desviacion; 10 int moda, frecuencia, frecuencia_moda, mediana; 11 12 /* Lectura de edades */ 13 personas = 0; 14 for (i=0; i<MAX_PERSONAS; i++) { 15 printf ("Introduce edad de la persona %d (si es negativa, acabar): ", i+1); 16 scanf ("%d", &edad[i]); 17 if (edad[i] < 0) 18 break; 19 personas++; 20 } 21 22 ... 23 24 return 0; 25 } La constante que hasta ahora se llamaba PERSONAS ha pasado a llamarse MAX_PERSONAS. Se pretende reflejar que su valor es la m´axima cantidad de edades de personas que podemos manejar, pues el n´umero de edades que manejamos realmente pasa a estar en la variable entera personas. Una forma alternativa de hacer lo mismo nos permite prescindir del ´ındice i: edades 6.c edades.c 1 #include <stdio.h> 2 #include <math.h> 3 4 #define MAX_PERSONAS 20 5 6 int main(void) 7 { 8 int edad[MAX_PERSONAS], personas, j, aux, suma_edad; 9 float suma_desviacion, media, desviacion; 10 int moda, frecuencia, frecuencia_moda, mediana; 74 Introducci´on a la Programaci´on con C
  • 81. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros 11 12 /* Lectura de edades */ 13 personas = 0; 14 do { 15 printf ("Introduce edad de la persona %d (si es negativa, acabar): ", personas+1); 16 scanf ("%d", &edad[personas]); 17 personas++; 18 } while (personas < MAX_PERSONAS && edad[personas-1] >= 0); 19 personas--; 20 21 ... 22 23 return 0; 24 } Imagina que se han introducido edades de 10 personas. La variable personas apunta (con- ceptualmente) al final de la serie de valores que hemos de considerar para efectuar los c´alculos pertinentes: edad 6 0 18 1 30 2 18 3 19 4 19 5 31 6 1 7 27 8 66 9 -1 10 887 11 -55 12 0 13 391 14 0 15 -6 16 89 17 322 18 -2 19 10personas 20MAX PERSONAS Ya podemos calcular la edad media, pero con un cuidado especial por las posibles divisiones por cero que provocar´ıa que el usuario escribiera una edad negativa como edad de la primera persona (en cuyo caso personas valdr´ıa 0): edades 7.c edades.c 1 #include <stdio.h> 2 #include <math.h> 3 4 #define MAX_PERSONAS 20 5 6 int main(void) 7 { 8 int edad[MAX_PERSONAS], personas, i, j, aux, suma_edad; 9 float suma_desviacion, media, desviacion; 10 int moda, frecuencia, frecuencia_moda, mediana; 11 12 /* Lectura de edades */ 13 personas = 0; 14 do { 15 printf ("Introduce edad de la persona %d (si es negativa, acabar): ", personas+1); 16 scanf ("%d", &edad[personas]); 17 personas++; 18 } while (personas < MAX_PERSONAS && edad[personas-1] >= 0); 19 personas--; 20 21 if (personas > 0) { 22 /* C´alculo de la media */ 23 suma_edad = 0; 24 for (i=0; i<personas ; i++) 25 suma_edad += edad[i]; 26 media = suma_edad / (float) personas ; 27 28 /* C´alculo de la desviacion t´ıpica */ 29 suma_desviacion = 0.0; 30 for (i=0; i<personas ; i++) 31 suma_desviacion += (edad[i] - media) * (edad[i] - media); 32 desviacion = sqrt( suma_desviacion / personas ); 33 Introducci´on a la Programaci´on con C 75
  • 82. 2.1 Vectores est´aticos 34 /* C´alculo de la moda */ 35 for (i=0; i<personas -1; i++) // Ordenaci´on mediante burbuja. 36 for (j=0; j<personas -i; j++) 37 if (edad[j] > edad[j+1]) { 38 aux = edad[j]; 39 edad[j] = edad[j+1]; 40 edad[j+1] = aux; 41 } 42 43 frecuencia = 0; 44 frecuencia_moda = 0; 45 moda = -1; 46 for (i=0; i<personas -1; i++) 47 if (edad[i] == edad[i+1]) 48 if (++frecuencia > frecuencia_moda) { 49 frecuencia_moda = frecuencia; 50 moda = edad[i]; 51 } 52 else 53 frecuencia = 0; 54 55 /* C´alculo de la mediana */ 56 mediana = edad[personas /2]; 57 58 /* Impresi´on de resultados */ 59 printf ("Edad media : %fn", media); 60 printf ("Desv. t´ıpica: %fn", desviacion); 61 printf ("Moda : %dn", moda); 62 printf ("Mediana : %dn", mediana); 63 } 64 else 65 printf ("No se introdujo dato alguno.n"); 66 67 return 0; 68 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 74 Cuando el n´umero de edades es par no hay elemento central en el vector ordenado, as´ı que estamos escogiendo la mediana como uno cualquiera de los elementos ((centrales)). Utiliza una definici´on alternativa de edad mediana que considera que su valor es la media de las dos edades que ocupan las posiciones m´as pr´oximas al centro. · 75 Modifica el ejercicio anterior para que, caso de haber dos o m´as valores con la m´axima frecuencia de aparici´on, se muestren todos por pantalla al solicitar la moda. · 76 Modifica el programa anterior para que permita efectuar c´alculos con hasta 100 personas. · 77 Modifica el programa del ejercicio anterior para que muestre, adem´as, cu´antas edades hay entre 0 y 9 a˜nos, entre 10 y 19, entre 20 y 29, etc. Considera que ninguna edad es igual o superior a 150. Ejemplo: si el usuario introduce las siguientes edades correspondientes a 12 personas: 10 23 15 18 20 18 57 12 29 31 78 28 el programa mostrar´a (adem´as de la media, desviaci´on t´ıpica, moda y mediana), la siguiente tabla: 0 - 9: 0 10 - 19: 5 20 - 29: 4 30 - 39: 1 40 - 49: 0 50 - 59: 1 60 - 69: 0 70 - 79: 1 76 Introducci´on a la Programaci´on con C
  • 83. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros 80 - 89: 0 90 - 99: 0 100 - 109: 0 110 - 119: 0 120 - 129: 0 130 - 139: 0 140 - 149: 0 · 78 Modifica el programa para que muestre un histograma de edades. La tabla anterior se mostrar´a ahora como este histograma: 0 - 9: 10 - 19: ***** 20 - 29: **** 30 - 39: * 40 - 49: 50 - 59: * 60 - 69: 70 - 79: * 80 - 89: 90 - 99: 100 - 109: 110 - 119: 120 - 129: 130 - 139: 140 - 149: Como puedes ver, cada asterisco representa la edad de una persona. · 79 Modifica el programa anterior para que el primer y ´ultimo rangos de edades mostrados en el histograma correspondan a tramos de edades en los que hay al menos una persona. El histograma mostrado antes aparecer´a ahora as´ı: 10 - 19: ***** 20 - 29: **** 30 - 39: * 40 - 49: 50 - 59: * 60 - 69: 70 - 79: * · 80 Modifica el programa del ejercicio anterior para que muestre el mismo histograma de esta otra forma: | ######### | | | | | | | | ######### | ######### | | | | | | | ######### | ######### | | | | | | | ######### | ######### | | | | | | | ######### | ######### | ######### | | ######### | | ######### | +-----------+-----------+-----------+-----------+-----------+-----------+-----------+ | 10 - 19 | 20 - 29 | 30 - 39 | 40 - 49 | 50 - 59 | 60 - 69 | 70 - 79 | . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1.5. Otro programa de ejemplo: una calculadora para polinomios Deseamos implementar una calculadora para polinomios de grado menor o igual que 10. Un polinomio p(x) = p0 + p1x + p2x2 + p3x3 + · · · + p10x10 puede representarse con un vector en el que se almacenan sus 11 coeficientes (p0, p1, . . . , p10). Vamos a construir un programa C que permita leer por teclado dos polinomios p(x) y q(x) y, una vez le´ıdos, calcule los polinomios s(x) = p(x) + q(x) y m(x) = p(x) · q(x). Empezaremos definiendo dos vectores p y q que han de poder contener 11 valores en coma flotante: Introducci´on a la Programaci´on con C 77
  • 84. 2.1 Vectores est´aticos polinomios.c 1 #include <stdio.h> 2 #define TALLA_POLINOMIO 11 3 4 int main(void) 5 { 6 float p[TALLA_POLINOMIO], q[TALLA_POLINOMIO]; 7 ... Como leer por teclado 11 valores para p y 11 m´as para q es innecesario cuando trabajamos con polinomios de grado menor que 10, nuestro programa leer´a los datos pidiendo en primer lugar el grado de cada uno de los polinomios y solicitando ´unicamente el valor de los coeficientes de grado menor o igual que el indicado: E polinomios.c E 1 #include <stdio.h> 2 3 #define TALLA_POLINOMIO 11 4 5 int main(void) 6 { 7 float p[TALLA_POLINOMIO], q[TALLA_POLINOMIO]; 8 int grado; 9 int i; 10 11 /* Lectura de p */ 12 do { 13 printf ("Grado de p (entre 0 y %d): ", TALLA_POLINOMIO-1); scanf ("%d", &grado); 14 } while (grado < 0 || grado >= TALLA_POLINOMIO); 15 for (i = 0; i<=grado; i++) { 16 printf ("p %d: ", i); scanf ("%f", &p[i]); 17 } 18 19 /* Lectura de q */ 20 do { 21 printf ("Grado de q (entre 0 y %d): ", TALLA_POLINOMIO-1); scanf ("%d", &grado); 22 } while (grado < 0 || grado >= TALLA_POLINOMIO); 23 for (i = 0; i<=grado; i++) { 24 printf ("q %d: ", i); scanf ("%f", &q[i]); 25 } 26 27 return 0; 28 } El programa presenta un problema: no inicializa los coeficientes que correponden a los t´erminos xn , para n mayor que el grado del polinomio. Como dichos valores deben ser nu- los, hemos de inicializarlos expl´ıcitamente (en aras de la brevedad mostramos ´unicamente la inicializaci´on de los coeficientes de p): polinomios.c 4 ... 5 int main(void) 6 { 7 float p[TALLA_POLINOMIO], q[TALLA_POLINOMIO]; 8 int grado; 9 int i; 10 11 /* Lectura de p */ 12 do { 13 printf ("Grado de p (entre 0 y %d): ", TALLA_POLINOMIO-1); scanf ("%d", &grado); 14 } while (grado < 0 || grado >= TALLA_POLINOMIO); 15 for (i = 0; i<=grado; i++) { 16 printf ("p %d: ", i); scanf ("%f", &p[i]); 17 } 78 Introducci´on a la Programaci´on con C
  • 85. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros 18 for (i=grado+1; i<TALLA_POLINOMIO; i++) 19 p[i] = 0.0; 20 ... 21 return 0; 22 } Ahora que hemos le´ıdo los polinomios, calculemos la suma. La almacenaremos en un nuevo vector llamado s. La suma de dos polinomios de grado menor que TALLA_POLINOMIO es un polinomio de grado tambi´en menor que TALLA_POLINOMIO, as´ı que el vector s tendr´a talla TALLA_POLINOMIO. polinomios.c 4 ... 5 int main(void) 6 { 7 float p[TALLA_POLINOMIO], q[TALLA_POLINOMIO], s[TALLA_POLINOMIO]; 8 ... El procedimiento para calcular la suma de polinomios es sencillo. He aqu´ı el c´alculo y la presentaci´on del resultado en pantalla: polinomios.c polinomios.c 1 #include <stdio.h> 2 3 #define TALLA_POLINOMIO 11 4 5 int main(void) 6 { 7 float p[TALLA_POLINOMIO], q[TALLA_POLINOMIO], s[TALLA_POLINOMIO]; 8 int grado; 9 int i; 10 11 /* Lectura de p */ 12 do { 13 printf ("Grado de p (entre 0 y %d): ", TALLA_POLINOMIO-1); scanf ("%d", &grado); 14 } while (grado < 0 || grado >= TALLA_POLINOMIO); 15 for (i = 0; i<=grado; i++) { 16 printf ("p %d: ", i); scanf ("%f", &p[i]); 17 } 18 for (i=grado+1; i<TALLA_POLINOMIO; i++) 19 p[i] = 0.0; 20 21 /* Lectura de q */ 22 do { 23 printf ("Grado de q (entre 0 y %d): ", TALLA_POLINOMIO-1); scanf ("%d", &grado); 24 } while (grado < 0 || grado >= TALLA_POLINOMIO); 25 for (i = 0; i<=grado; i++) { 26 printf ("q %d: ", i); scanf ("%f", &q[i]); 27 } 28 for (i=grado+1; i<TALLA_POLINOMIO; i++) 29 q[i] = 0.0; 30 31 /* C´alculo de la suma */ 32 for (i=0; i<TALLA_POLINOMIO; i++) 33 s[i] = p[i] + q[i]; 34 35 /* Presentaci´on del resultado */ 36 printf ("Suma: %f ", s[0]); 37 for (i=1; i<TALLA_POLINOMIO; i++) 38 printf ("+ %f x^%d ", s[i], i); 39 printf ("n"); 40 41 return 0; 42 } Introducci´on a la Programaci´on con C 79
  • 86. 2.1 Vectores est´aticos Aqu´ı tienes un ejemplo de uso del programa con los polinomios p(x) = 5 + 3x + 5x2 + x3 y q(x) = 4 − 4x − 5x2 + x3 : Grado de p (entre 0 y 10): 3 p_0: 5 p_1: 3 p_2: 5 p_3: 1 Grado de q (entre 0 y 10): 3 q_0: 4 q_1: -4 q_2: -5 q_3: 1 Suma: 9.000000 + -1.000000 x^1 + 0.000000 x^2 + 2.000000 x^3 + 0.000000 x^4 + 0.000000 x^5 + 0.000000 x^6 + 0.000000 x^7 + 0.000000 x^8 + 0.000000 x^9 + 0.000000 x^10 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 81 Modifica el programa anterior para que no se muestren los coeficientes nulos. · 82 Tras efectuar los cambios propuestos en el ejercicio anterior no aparecer´a nada por pantalla cuando todos los valores del polinomio sean nulos. Modifica el programa para que, en tal caso, se muestre por pantalla 0.000000. · 83 Tras efectuar los cambios propuestos en los ejercicios anteriores, el polinomio empieza con un molesto signo positivo cuando s0 es nulo. Corrige el programa para que el primer t´ermino del polinomio no sea precedido por el car´acter +. · 84 Cuando un coeficiente es negativo, por ejemplo −1, el programa anterior muestra su correspondiente t´ermino en pantalla as´ı: + -1.000 x^1. Modifica el programa anterior para que un t´ermino con coeficiente negativo como el del ejemplo se muestre as´ı: - 1.000000 x^1. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Nos queda lo m´as dif´ıcil: el producto de los dos polinomios. Lo almacenaremos en un vector llamado m. Como el producto de dos polinomios de grado menor o igual que n es un polinomio de grado menor o igual que 2n, la talla del vector m no es TALLA_POLINOMIO: 1 ... 2 int main(void) 3 { 4 float p[TALLA_POLINOMIO], q[TALLA_POLINOMIO], s[TALLA_POLINOMIO]; 5 float m[2*TALLA_POLINOMIO-1]; 6 ... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 85 ¿Entiendes por qu´e hemos reservado 2*TALLA_POLINOMIO-1 elementos para m y no 2*TALLA_POLINOMIO? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . El coeficiente mi, para valores de i entre 0 y el grado m´aximo de m(x), es decir, entre los enteros 0 y 2*TALLA_POLINOMIO-2, se calcula as´ı: mi = i j=0 pj · qi−j. Deberemos tener cuidado de no acceder err´oneamente a elementos de p o q fuera del rango de ´ındices v´alidos. Implementemos ese c´alculo: polinomios 1.c polinomios.c 1 #include <stdio.h> 2 3 #define TALLA_POLINOMIO 11 4 80 Introducci´on a la Programaci´on con C
  • 87. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros 5 int main(void) 6 { 7 float p[TALLA_POLINOMIO], q[TALLA_POLINOMIO], s[TALLA_POLINOMIO]; 8 float m[2*TALLA_POLINOMIO-1]; 9 int grado; 10 int i, j; 11 12 /* Lectura de p */ 13 do { 14 printf ("Grado de p (entre 0 y %d): ", TALLA_POLINOMIO-1); scanf ("%d", &grado); 15 } while (grado < 0 || grado >= TALLA_POLINOMIO); 16 for (i = 0; i<=grado; i++) { 17 printf ("p %d: ", i); scanf ("%f", &p[i]); 18 } 19 for (i=grado+1; i<TALLA_POLINOMIO; i++) 20 p[i] = 0.0; 21 22 /* Lectura de q */ 23 do { 24 printf ("Grado de q (entre 0 y %d): ", TALLA_POLINOMIO-1); scanf ("%d", &grado); 25 } while (grado < 0 || grado >= TALLA_POLINOMIO); 26 for (i = 0; i<=grado; i++) { 27 printf ("q %d: ", i); scanf ("%f", &q[i]); 28 } 29 for (i=grado+1; i<TALLA_POLINOMIO; i++) 30 q[i] = 0.0; 31 32 /* C´alculo de la suma */ 33 for (i=0; i<TALLA_POLINOMIO; i++) 34 s[i] = p[i] + q[i]; 35 36 /* Presentaci´on del resultado */ 37 printf ("Suma: %f ", s[0]); 38 for (i=1; i<TALLA_POLINOMIO; i++) 39 printf ("+ %f x^%d ", s[i], i); 40 printf ("n"); 41 42 /* C´alculo del producto */ 43 for (i=0; i<2*TALLA_POLINOMIO-1; i++) { 44 m[i] = 0.0; 45 for (j=0; j<=i; j++) 46 if (j < TALLA_POLINOMIO && i-j < TALLA_POLINOMIO) 47 m[i] += p[j] * q[i-j]; 48 } 49 50 /* Presentaci´on del resultado */ 51 printf ("Producto: %f ", m[0]); 52 for (i=1; i<2*TALLA_POLINOMIO-1; i++) 53 printf ("+ %f x^%d ", m[i], i); 54 printf ("n"); 55 56 return 0; 57 } Observa que nos hubiera venido bien definir sendas funciones para la lectura y escritura de los polinomios, pero al no saber definir funciones todav´ıa, hemos tenido que copiar dos veces el fragmento de programa correspondiente. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 86 El programa que hemos dise˜nado es ineficiente. Si, por ejemplo, trabajamos con polino- mios de grado 5, sigue operando con los coeficientes correspondientes a x6 , x7 ,. . . , x10 , que son nulos. Modifica el programa para que, con la ayuda de variables enteras, recuerde el grado de los polinomios p(x) y q(x) en sendas variables talla_p y talla_q y use esta informaci´on en los c´alculos de modo que se opere ´unicamente con los coeficientes de los t´erminos de grado menor Introducci´on a la Programaci´on con C 81
  • 88. 2.1 Vectores est´aticos o igual que el grado del polinomio. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ahora que hemos presentado tres programas ilustrativos del uso de vectores en C, f´ıjate en que: El tama˜no de los vectores siempre se determina en tiempo de compilaci´on. En un vector podemos almacenar una cantidad de elementos menor o igual que la decla- rada en su capacidad, nunca mayor. Si almacenamos menos elementos de los que caben (como en el programa que efect´ua estad´ısticas de una serie de edades), necesitas alguna variable auxiliar que te permita saber en todo momento cu´antas de las celdas contienen informaci´on. Si a˜nades un elemento, has de incrementar t´u mismo el valor de esa variable. Ya sabes lo suficiente sobre vectores para poder hacer frente a estos ejercicios: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 87 Dise˜na un programa que pida el valor de 10 n´umeros enteros distintos y los almacene en un vector. Si se da el caso, el programa advertir´a al usuario, tan pronto sea posible, si introduce un n´umero repetido y solicitar´a nuevamente el n´umero hasta que sea diferente de todos los anteriores. A continuaci´on, el programa mostrar´a los 10 n´umeros por pantalla. · 88 En una estaci´on meteorol´ogica registramos la temperatura (en grados cent´ıgrados) cada hora durante una semana. Almacenamos el resultado en un vector de 168 componentes (que es el resultado del producto 7 × 24). Dise˜na un programa que lea los datos por teclado y muestre: La m´axima y m´ınima temperaturas de la semana. La m´axima y m´ınima temperaturas de cada d´ıa. La temperatura media de la semana. La temperatura media de cada d´ıa. El n´umero de d´ıas en los que la temperatura media fue superior a 30 grados. El d´ıa y hora en que se registr´o la mayor temperatura. · 89 La cabecera stdlib.h incluye la declaraci´on de funciones para generar n´umeros aleato- rios. La funci´on rand, que no tiene par´ametros, devuelve un entero positivo aleatorio. Si deseas generar n´umeros aleatorios entre 0 y un valor dado N, puedes evaluar rand() % (N+1). Cuando ejecutas un programa que usa rand, la semilla del generador de n´umeros aleatorios es siempre la misma, as´ı que acabas obteniendo la misma secuencia de n´umeros aleatorios. Puedes cambiar la semilla del generador de n´umeros aleatorios pas´andole a la funci´on srand un n´umero entero sin signo. Usa el generador de n´umeros aleatorios para inicializar un vector de 10 elementos con n´umeros enteros entre 0 y 4. Muestra por pantalla el resultado. Detecta y muestra, a con- tinuaci´on, el tama˜no de la sucesi´on m´as larga de n´umeros consecutivos iguales. (Ejemplo: si los n´umeros generados son 0 4 3 3 2 1 3 2 2 2, el tramo m´as largo formado por n´umeros iguales es de talla 3 (los tres doses al final de la secuencia), as´ı que por pantalla aparecer´a el valor 3.) · 90 Modifica el ejercicio anterior para que trabaje con un vector de 100 elementos. · 91 Genera un vector con 20 n´umeros aleatorios entre 0 y 100 y muestra por pantalla el vector resultante y la secuencia de n´umeros crecientes consecutivos m´as larga. (Ejemplo: la secuencia 1 33 73 85 87 93 99 es la secuencia creciente m´as larga en la serie de n´umeros 87 45 34 12 1 33 73 85 87 93 99 0 100 65 32 17 29 16 12 0.) · 92 Escribe un programa C que ejecute 1000 veces el c´alculo de la longitud de la secuencia m´as larga sobre diferentes secuencias aleatorias (ver ejercicio anterior) y que muestre la longitud media y desviaci´on t´ıpica de dichas secuencias. · 93 Genera 100 n´umeros aleatorios entre 0 y 1000 y almac´enalos en un vector. Determina a continuaci´on qu´e n´umeros aparecen m´as de una vez. 82 Introducci´on a la Programaci´on con C
  • 89. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros · 94 Genera 100 n´umeros aleatorios entre 0 y 10 y almac´enalos en un vector. Determina a continuaci´on cu´al es el n´umero que aparece m´as veces. · 95 Dise˜na un programa C que almacene en un vector los 100 primeros n´umeros primos. · 96 Dise˜na un programa C que lea y almacene en un vector 10 n´umeros enteros asegur´andose de que sean positivos. A continuaci´on, el programa pedir´a que se introduzca una serie de n´umeros enteros y nos dir´a si cada uno de ellos est´a o no en el vector. El programa finaliza cuando el usuario introduce un n´umero negativo. · 97 Dise˜na un programa C que lea y almacene en un vector 10 n´umeros enteros asegur´andose de que sean positivos. A continuaci´on, el programa pedir´a que se introduzca una serie de n´umeros enteros y nos dir´a si cada uno de ellos est´a o no en el vector. El programa finaliza cuando el usuario introduce un n´umero negativo. Debes ordenar por el m´etodo de la burbuja el vector de 10 elementos tan pronto se conocen sus valores. Cuando debas averiguar si un n´umero est´a o no en el vector, utiliza el algoritmo de b´usqueda dicot´omica. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1.6. Disposici´on de los vectores en memoria Es importante que conozcas bien c´omo se disponen los vectores en memoria. Cuando se encuen- tra esta declaraci´on en un programa: int a[5]; el compilador reserva una zona de memoria contigua capaz de albergar 5 valores de tipo int. Como una variable de tipo int ocupa 4 bytes, el vector a ocupar´a 20 bytes. Podemos comprobarlo con este programa: 1 #include <stdio.h> 2 3 #define TALLA 5 4 5 int main(void) 6 { 7 int a[TALLA]; 8 9 printf ("Ocupaci´on de un elemento de a (en bytes): %dn", sizeof(a[0])); 10 printf ("Ocupaci´on de a (en bytes): %dn", sizeof(a)); 11 return 0; 12 } El resultado de ejecutarlo es ´este: Ocupaci´on de un elemento de a (en bytes): 4 Ocupaci´on de a (en bytes): 20 Cada byte de la memoria tiene una direcci´on. Si, pongamos por caso, el vector a empieza en la direcci´on 1000, a[0] se almacena en los bytes 1000–1003, a[1] en los bytes 1004–1007, y as´ı sucesivamente. El ´ultimo elemento, a[4], ocupar´a los bytes 1016–1019: 996: 1000: a[0] 1004: a[1] 1008: a[2] 1012: a[3] 1016: a[4] 1020: Introducci´on a la Programaci´on con C 83
  • 90. 2.1 Vectores est´aticos Big-endian y little-endian Lo bueno de los est´andares es. . . que hay muchos donde elegir. No hay forma de ponerse de acuerdo. Muchos ordenadores almacenan los n´umeros enteros de m´as de 8 bits disponiendo los bits m´as significativos en la direcci´on de memoria m´as baja y otros, en la m´as alta. Los primeros se dice que siguen la codificaci´on ((big-endian)) y los segundos, ((little-endian)). Pongamos un ejemplo. El n´umero 67586 se representa en binario con cuatro bytes: 00000000 00000001 00001000 00000010 Supongamos que ese valor se almacena en los cuatro bytes que empiezan en la direcci´on 1000. En un ordenador ((big-endian)), se dispondr´ıan en memoria as´ı (te indicamos bajo cada byte su direcci´on de memoria): 1000: 00000000 00000001 00001000 00000010 1000 1001 1002 1003 En un ordenador ((little-endian)), por contra, se representar´ıa de esta otra forma: 1000: 00000010 00001000 00000001 00000000 1000 1001 1002 1003 Los ordenadores PC (que usan microprocesadores Intel y AMD), por ejemplo, son ((little- endian)) y los Macintosh basados en microprocesadores Motorola son ((big-endian)). Aunque nosotros trabajamos en clase con ordenadores Intel, te mostraremos los valores binarios como est´as acostumbrado a verlos: con el byte m´as significativo a la izquierda. La diferente codificaci´on de unas y otras plataformas plantea serios problemas a la hora de intercambiar informaci´on en ficheros binarios, es decir, ficheros que contienen volcados de la informaci´on en memoria. Nos detendremos nuevamente sobre esta cuesti´on cuando estudiamos ficheros. Por cierto, lo de ((little-endian)) y ((big-endian)) viene de ((Los viajes de Gulliver)), la novela de Johnathan Swift. En ella, los liliputienses debaten sobre una importante cuesti´on pol´ıtica: ¿deben abrirse los huevos pasados por agua por su extremo grande, como defiende el partido Big-Endian, o por su extremo puntiagudo, como mantiene el partido Little-Endian? ¿Recuerdas el operador & que te presentamos en el cap´ıtulo anterior? Es un operador unario que permite conocer la direcci´on de memoria de una variable. Puedes aplicar el operador & a un elemento del vector. Por ejemplo, &a[2] es la direcci´on de memoria en la que empieza a[2], es decir, la direcci´on 1008 en el ejemplo. Veamos qu´e direcci´on ocupa cada elemento de un vector cuando ejecutamos un programa sobre un computador real: direcciones vector.c direcciones vector.c 1 #include <stdio.h> 2 3 #define TALLA 5 4 5 int main(void) 6 { 7 int a[TALLA], i; 8 9 for (i = 0; i < TALLA; i++) 10 printf ("Direcci´on de a[%d]: %un", i, (unsigned int) &a[i]); 11 12 return 0; 13 } Al ejecutar el programa obtenemos en pantalla lo siguiente (puede que obtengas un resultado diferente si haces la prueba t´u mismo, pues el vector puede estar en un lugar cualquiera de la memoria): Direcci´on de a[0]: 3221222640 Direcci´on de a[1]: 3221222644 Direcci´on de a[2]: 3221222648 84 Introducci´on a la Programaci´on con C
  • 91. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros Direcci´on de a[3]: 3221222652 Direcci´on de a[4]: 3221222656 ¿Ves? Cada direcci´on de memoria de una celda de a se diferencia de la siguiente en 4 unidades. Recuerda que la funci´on de lectura de datos por teclado scanf modifica el valor de una variable cuya direcci´on de memoria se le suministra. Para depositar en la zona de memoria de la variable el nuevo valor necesita conocer la direcci´on de memoria. Por esa raz´on preced´ıamos los identificadores de las variables con el operador &. Este programa, por ejemplo, lee por teclado el valor de todos los componentes de un vector utilizando el operador & para conocer la direcci´on de memoria de cada uno de ellos: lee vector.c lee vector.c 1 #include <stdio.h> 2 3 #define TALLA 5 4 5 int main(void) 6 { 7 int a[TALLA], i; 8 9 for (i = 0; i < TALLA; i++) 10 printf ("Introduce el valor de a[%d]:", i); scanf ("%d", &a[i]); 11 12 return 0; 13 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 98 ¿Qu´e problema presenta esta otra versi´on del mismo programa? lee vector 1.c lee vector.c 1 #include <stdio.h> 2 3 #define TALLA 5 4 5 int main(void) 6 { 7 int a[TALLA], i; 8 9 for (i = 0; i < TALLA; i++) 10 printf ("Introduce el valor de a[%d]:", i); scanf ("%d", a[i]); 11 12 return 0; 13 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Analiza este programa: direcciones vector2.c direcciones vector2.c 1 #include <stdio.h> 2 3 #define TALLA 5 4 5 int main(void) 6 { 7 int a[TALLA], i; 8 9 for (i = 0; i < TALLA; i++) 10 printf ("Direcci´on de a[%d]: %un", i, (unsigned int) &a[i]); 11 12 printf ("Direcci´on de a: %un", (unsigned int) a); 13 return 0; 14 } He aqu´ı el resultado de ejecutarlo: Introducci´on a la Programaci´on con C 85
  • 92. 2.1 Vectores est´aticos Direcci´on de a[0]: 3221222640 Direcci´on de a[1]: 3221222644 Direcci´on de a[2]: 3221222648 Direcci´on de a[3]: 3221222652 Direcci´on de a[4]: 3221222656 Direcci´on de a: 3221222640 Observa que la direcci´on de memoria de las l´ıneas primera y ´ultima es la misma. En conse- cuencia, esta l´ınea: 1 printf ("Direcci´on de a: %un", (unsigned int) &a[0]); es equivalente a esta otra: 1 printf ("Direcci´on de a: %un", (unsigned int) a); As´ı pues, a expresa una direcci´on de memoria (la de su primer elemento), es decir, a es un puntero o referencia a memoria y es equivalente a &a[0]. La caracter´ıstica de que el identificador de un vector represente, a la vez, al vector y a un puntero que apunta donde empieza el vector recibe el nombre dualidad vector-puntero, y es un rasgo propio del lenguaje de programaci´on C. Representaremos esquem´aticamente los vectores de modo similar a como represent´abamos las listas en Python: a 0 0 0 1 0 2 0 3 0 4 F´ıjate en que el gr´afico pone claramente de manifiesto que a es un puntero, pues se le representa con una flecha que apunta a la zona de memoria en la que se almacenan los elementos del vector. Nos interesa dise˜nar programas con un nivel de abstracci´on tal que la imagen conceptual que tengamos de los vectores se limite a la del diagrama. Mentiremos cada vez menos Lo cierto es que a no es exactamente un puntero, aunque funciona como tal. Ser´ıa m´as justo representar la memoria as´ı: a 0 0 0 1 0 2 0 3 0 4 Pero, por el momento, conviene que consideres v´alida la representaci´on en la que a es un puntero. Cuando estudiemos la gesti´on de memoria din´amica abundaremos en esta cuesti´on. Recuerda que el operador & obtiene la direcci´on de memoria en la que se encuentra un valor. En esta figura te ilustramos &a[0] y &a[2] como sendos punteros a sus respectivas celdas en el vector. &a[2] a 0 0 0 1 0 2 0 3 0 4 &a[0] ¿C´omo ((encuentra)) C la direcci´on de memoria de un elemento del vector cuando accedemos a trav´es de un ´ındice? Muy sencillo, efectuando un c´alculo consistente en sumar al puntero que se˜nala el principio del vector el resultado de multiplicar el ´ındice por el tama˜no de un elemento del vector. La expresi´on a[2], por ejemplo, se entiende como ((accede al valor de tipo int que empieza en la direcci´on a con un desplazamiento de 2 × 4 bytes)). Una sentencia de asignaci´on como a[2] = 0 se interpreta como ((almacena el valor 0 en el entero int que empieza en la direcci´on de memoria de a m´as 2 × 4 bytes)). 86 Introducci´on a la Programaci´on con C
  • 93. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros 2.1.7. Algunos problemas de C: accesos il´ıcitos a memoria Aqu´ı tienes un programa con un resultado que puede sorprenderte: ilicito.c ilicito.c 1 #include <stdio.h> 2 3 #define TALLA 3 4 5 int main(void) 6 { 7 int v[TALLA], w[TALLA], i; 8 9 for(i=0; i<TALLA; i++) { 10 v[i] = i; 11 w[i] = 10 + i; 12 } 13 14 printf ("+--------+----------------------+-------+n"); 15 printf ("| Objeto | Direcci´on de memoria | Valor |n"); 16 printf ("+--------+----------------------+-------+n"); 17 printf ("| i | %20u | %5d |n", (unsigned int) &i, i); 18 printf ("+--------+----------------------+-------+n"); 19 printf ("| w[0] | %20u | %5d |n", (unsigned int) &w[0], w[0]); 20 printf ("| w[1] | %20u | %5d |n", (unsigned int) &w[1], w[1]); 21 printf ("| w[2] | %20u | %5d |n", (unsigned int) &w[2], w[2]); 22 printf ("+--------+----------------------+-------+n"); 23 printf ("| v[0] | %20u | %5d |n", (unsigned int) &v[0], v[0]); 24 printf ("| v[1] | %20u | %5d |n", (unsigned int) &v[1], v[1]); 25 printf ("| v[2] | %20u | %5d |n", (unsigned int) &v[2], v[2]); 26 printf ("+--------+----------------------+-------+n"); 27 printf ("| v[-2] | %20u | %5d |n", (unsigned int) &v[-2], v[-2]); 28 printf ("| v[-3] | %20u | %5d |n", (unsigned int) &v[-3], v[-3]); 29 printf ("| v[-4] | %20u | %5d |n", (unsigned int) &v[-4], v[-4]); 30 printf ("| w[5] | %20u | %5d |n", (unsigned int) &w[5], w[5]); 31 printf ("| w[-1] | %20u | %5d |n", (unsigned int) &w[-1], w[-1]); 32 printf ("| v[-5] | %20u | %5d |n", (unsigned int) &v[-5], v[-5]); 33 printf ("+--------+----------------------+-------+n"); 34 35 return 0; 36 } Aqu´ı tienes el resultado de su ejecuci´on3 : +--------+----------------------+-------+ | Objeto | Direcci´on de memoria | Valor | +--------+----------------------+-------+ | i | 3221222636 | 3 | +--------+----------------------+-------+ | w[0] | 3221222640 | 10 | | w[1] | 3221222644 | 11 | | w[2] | 3221222648 | 12 | +--------+----------------------+-------+ | v[0] | 3221222656 | 0 | | v[1] | 3221222660 | 1 | | v[2] | 3221222664 | 2 | +--------+----------------------+-------+ | v[-2] | 3221222648 | 12 | | v[-3] | 3221222644 | 11 | | v[-4] | 3221222640 | 10 | | w[5] | 3221222660 | 1 | 3Nuevamente, una advertencia: puede que obtengas un resultado diferente al ejecutar el programa en tu ordenador. La asignaci´on de direcciones de memoria a cada objeto de un programa es una decisi´on que adopta el compilador con cierta libertad. Introducci´on a la Programaci´on con C 87
  • 94. 2.1 Vectores est´aticos | w[-1] | 3221222636 | 3 | | v[-5] | 3221222636 | 3 | +--------+----------------------+-------+ La salida es una tabla con tres columnas: en la primera se indica el objeto que se est´a estudiando, la segunda corresponde a la direcci´on de memoria de dicho objeto4 y la tercera muestra el valor almacenado en dicho objeto. A la vista de las direcciones de memoria de los objetos i, v[0], v[1], v[2], w[0], w[1] y w[2], el compilador ha reservado la memoria de estas variables as´ı: 3221222636: i3 3221222640: w[0]10 3221222644: w[1]11 3221222648: w[2]12 3221222652: 3221222656: v[0]0 3221222660: v[1]1 3221222664: v[2]2 F´ıjate en que las seis ´ultimas filas de la tabla corresponden a accesos a v y w con ´ındices fuera de rango. Cuando trat´abamos de acceder a un elemento inexistente en una lista Python, el int´erprete generaba un error de tipo (error de ´ındice). Ante una situaci´on similar, C no detecta error alguno. ¿Qu´e hace, pues? Aplica la f´ormula de indexaci´on, sin m´as. Estudiemos con calma el primer caso extra˜no: v[-2]. C lo interpreta como: ((acceder al valor almacenado en la direcci´on que resulta de sumar 3221222656 (que es donde empieza el vector v) a (−2)×4 (−2 es el ´ındice del vector y 4 es tama˜no de un int))). Haz el c´alculo: el resultado es 3221222648. . . ¡la misma direcci´on de memoria que ocupa el valor de w[2]! Esa es la raz´on de que se muestre el valor 12. En la ejecuci´on del programa, v[-2] y w[2] son exactamente lo mismo. Encuentra t´u mismo una explicaci´on para los restantes accesos il´ıcitos. ¡Ojo! Que se pueda hacer no significa que sea aconsejable hacerlo. En absoluto. Es m´as: debes evitar acceder a elementos con ´ındices de vector fuera de rango. Si no conviene hacer algo as´ı, ¿por qu´e no comprueba C si el ´ındice est´a en el rango correcto antes de acceder a los elementos y, en caso contrario, nos se˜nala un error? Por eficiencia. Un programa que maneje vectores acceder´a a sus elementos, muy probablemente, en numerosas ocasiones. Si se ha de comprobar si el ´ındice est´a en el rango de valores v´alidos, cada acceso se penalizar´a con un par de comparaciones y el programa se ejecutar´a m´as lentamente. C sacrifica seguridad por velocidad, de ah´ı que tenga cierta fama (justificad´ısma) de lenguaje ((peligroso)). 2.1.8. Asignaci´on y copia de vectores Este programa pretende copiar un vector en otro, pero es incorrecto: copia vectores mal.c E copia vectores mal.c E 1 #define TALLA 10 2 3 int main(void) 4 { 5 int original[TALLA] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10} ; 6 int copia[TALLA]; 7 8 copia = original; 9 10 return 0; 11 } 4Si ejecutas el programa en tu ordenador, es probable que obtengas valores distintos para las direcciones de memoria. Es normal: en cada ordenador y con cada ejecuci´on se puede reservar una zona de memoria distinta para los datos. 88 Introducci´on a la Programaci´on con C
  • 95. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros Violaci´on de segmento Los errores de acceso a zonas de memoria no reservada se cuentan entre los peores. En el ejemplo, hemos accedido a la zona de memoria de un vector sali´endonos del rango de indexaci´on v´alido de otro, lo cual ha producido resultados desconcertantes. Pero podr´ıa habernos ido a´un peor: si tratas de escribir en una zona de memoria que no pertenece a ninguna de tus variables, cosa que puedes hacer asignando un valor a un elemento de vector fuera de rango, es posible que se genere una excepci´on durante la ejecuci´on del programa: intentar escribir en una zona de memoria que no ha sido asignada a nuestro proceso dispara, en Unix, una se˜nal de ((violaci´on de segmento)) (segmentation violation) que provoca la inmediata finalizaci´on de la ejecuci´on del programa. F´ıjate en este programa: violacion.c E violacion.c E 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a[10]; 6 7 a[10000] = 1; 8 9 return 0; 10 } Cuando lo ejecutamos en un ordenador bajo Unix, obtenemos este mensaje por pantalla: Violaci´on de segmento El programa ha finalizado abruptamente al ejecutar la asignaci´on de la l´ınea 7. Estos errores en la gesti´on de memoria se manifiestan de formas muy variadas: pue- den producir resultados extra˜nos, finalizar la ejecuci´on incorrectamente o incluso bloquear al computador. ¿Bloquear al computador? S´ı, en sistemas operativos poco robustos, como Microsoft Windows, el ordenador puede quedarse bloqueado. (Probablemente has experi- mentado la sensaci´on usando algunos programas comerciales en el entorno Microsoft Win- dows.) Ello se debe a que ciertas zonas de memoria deber´ıan estar fuera del alcance de los programas de usuario y el sistema operativo deber´ıa prohibir accesos il´ıcitos. Unix mata al proceso que intenta efectuar accesos il´ıcitos (de ah´ı que terminen con mensajes como ((Violaci´on de segmento))). Microsoft Windows no tiene la precauci´on de protegerlas, as´ı que las consecuencias son mucho peores. Pero casi lo peor es que tu programa puede funcionar mal en unas ocasiones y bien en otras. El hecho de que el programa pueda funcionar mal algunas veces y bien el resto es peligros´ısimo: como los errores pueden no manifestarse durante el desarrollo del programa, cabe la posibilidad de que no los detectes. Nada peor que dar por bueno un programa que, en realidad, es incorrecto. Tenlo siempre presente: la gesti´on de vectores obliga a estar siempre pendiente de no rebasar la zona de memoria reservada. Si compilas el programa, obtendr´as un error en la l´ınea 8 que te impedir´a obtener un ejecuta- ble: ((incompatible types in assignment)). El mensaje de error nos indica que no es posible efectuar asignaciones entre tipos vectoriales. Nuestra intenci´on era que antes de ejecutar la l´ınea 8, la memoria presentara este aspecto: original 1 0 2 1 3 2 4 3 5 4 6 5 7 6 8 7 9 8 10 9 copia 0 1 2 3 4 5 6 7 8 9 y, una vez ejecutada la l´ınea 8 llegar a una de estas dos situaciones: 1. obtener en copia una copia del contenido de original: Introducci´on a la Programaci´on con C 89
  • 96. 2.1 Vectores est´aticos original 1 0 2 1 3 2 4 3 5 4 6 5 7 6 8 7 9 8 10 9 copia 1 0 2 1 3 2 4 3 5 4 6 5 7 6 8 7 9 8 10 9 2. o conseguir que, como en Python, copia apunte al mismo lugar que original: original 1 0 2 1 3 2 4 3 5 4 6 5 7 6 8 7 9 8 10 9 copia 0 1 2 3 4 5 6 7 8 9 Pero no ocurre ninguna de las dos cosas: el identificador de un vector est´atico se considera un puntero inmutable. Siempre apunta a la misma direcci´on de memoria. No puedes asignar un vector a otro porque eso significar´ıa cambiar el valor de su direcci´on. (Observa, adem´as, que en el segundo caso, la memoria asignada a copia quedar´ıa sin puntero que la referenciara.) Si quieres copiar el contenido de un vector en otro debes hacerlo elemento a elemento: copia vectores.c copia vectores.c 1 #define TALLA 10 2 3 int main(void) 4 { 5 int original[TALLA] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10} ; 6 int copia[TALLA]; 7 int i; 8 9 for (i=0; i<TALLA; i++) 10 copia[i] = original[i]; 11 12 return 0; 13 } 2.1.9. Comparaci´on de vectores En Python pod´ıamos comparar listas. Por ejemplo, [1,2,3] == [1,1+1,3] devolv´ıa True. Ya lo habr´as adivinado: C no permite comparar vectores. Efectivamente. Si quieres comparar dos vectores, has de hacerlo elemento a elemento: compara vectores.c compara vectores.c 1 #define TALLA 3 2 3 int main(void) 4 { 5 int original[TALLA] = { 1, 2, 3 }; 6 int copia[TALLA] = {1, 1+1, 3}; 7 int i, son_iguales; 8 9 son_iguales = 1; // Suponemos que todos los elementos son iguales dos a dos. 10 i = 0; 11 while (i < TALLA && son_iguales) { 12 if (copia[i] != original[i]) // Pero basta con que dos elementos no sean iguales... 13 son_iguales = 0; // ... para que los vectores sean distintos. 14 i++; 15 } 16 17 if (son_iguales) 18 printf ("Son igualesn"); 19 else 20 printf ("No son igualesn"); 90 Introducci´on a la Programaci´on con C
  • 97. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros 21 22 return 0; 23 } 2.2. Cadenas est´aticas Las cadenas son un tipo de datos b´asico en Python, pero no en C. Las cadenas de C son vectores de caracteres (elementos de tipo char) con una peculiaridad: el texto de la cadena termina siempre en un car´acter nulo. El car´acter nulo tiene c´odigo ASCII 0 y podemos representarlo tanto con el entero 0 como con el car´acter ’0’ (recuerda que ’0’ es una forma de escribir el valor entero 0). ¡Ojo! No confundas ’0’ con ’0’: el primero vale 0 y el segundo vale 48. Las cadenas est´aticas en C son, a diferencia de las cadenas Python, mutables. Eso significa que puedes modificar el contenido de una cadena durante la ejecuci´on de un programa. 2.2.1. Declaraci´on de cadenas Las cadenas se declaran como vectores de caracteres, as´ı que debes proporcionar el n´umero m´aximo de caracteres que es capaz de almacenar: su capacidad. Esta cadena, por ejemplo, se declara con capacidad para almacenar 10 caracteres: char a[10]; Puedes inicializar la cadena con un valor en el momento de su declaraci´on: char a[10] = "cadena"; Hemos declarado a como un vector de 10 caracteres y lo hemos inicializado asign´andole la cadena "cadena". F´ıjate: hemos almacenado en a una cadena de menos de 10 caracteres. No hay problema: la longitud de la cadena almacenada en a es menor que la capacidad de a. 2.2.2. Representaci´on de las cadenas en memoria A simple vista, "cadena" ocupa 6 bytes, pues contamos en ella 6 caracteres, pero no es as´ı. En realidad, "cadena" ocupa 7 bytes: los 6 que corresponden a los 6 caracteres que ves m´as uno correspondiente a un car´acter nulo al final, que se denomina terminador de cadena y es invisible. Al declarar e inicializar una cadena as´ı: char a[10] = "cadena"; la memoria queda de este modo: a c 0 a 1 d 2 e 3 n 4 a 5 0 6 7 8 9 Es decir, es como si hubi´esemos inicializado la cadena de este otro modo equivalente: 1 char a[10] = { ’c’, ’a’, ’d’, ’e’, ’n’, ’a’, ’0’ }; Recuerda, pues, que hay dos valores relacionados con el tama˜no de una cadena: su capacidad, que es la talla del vector de caracteres; su longitud, que es el n´umero de caracteres que contiene, sin contar el terminador de la cadena. La longitud de la cadena debe ser siempre estrictamente menor que la capacidad del vector para no desbordar la memoria reservada. ¿Y por qu´e toda esta complicaci´on del terminador de cadena? Lo normal al trabajar con una variable de tipo cadena es que su longitud var´ıe conforme evoluciona la ejecuci´on del programa, pero el tama˜no de un vector es fijo. Por ejemplo, si ahora tenemos en a el texto "cadena" y m´as tarde decidimos guardar en ella el texto "texto", que tiene un car´acter menos, estaremos pasando de esta situaci´on: Introducci´on a la Programaci´on con C 91
  • 98. 2.2 Cadenas est´aticas Una cadena de longitud uno no es un car´acter Hemos dicho en el cap´ıtulo anterior que una cadena de un s´olo car´acter, por ejemplo "y ", no es lo mismo que un car´acter, por ejemplo ’y’. Ahora puedes saber por qu´e: la diferencia estriba en que "y " ocupa dos bytes, el que corresponde al car´acter ’y’ y el que corresponde al car´acter nulo ’0’, mientras que ’y’ ocupa un solo byte. F´ıjate en esta declaraci´on de variables: 1 char a = ’y’; 2 char b[2] = "y"; He aqu´ı una representaci´on gr´afica de las variables y su contenido: ya b y 0 0 1 Recuerda: Las comillas simples definen un car´acter y un car´acter ocupa un solo byte. Las comillas dobles definen una cadena. Toda cadena incluye un car´acter nulo invisible al final. a c 0 a 1 d 2 e 3 n 4 a 5 0 6 7 8 9 a esta otra: a t 0 e 1 x 2 t 3 o 4 0 5 6 7 8 9 F´ıjate en que la zona de memoria asignada a a sigue siendo la misma. El ((truco)) del terminador ha permitido que la cadena decrezca. Podemos conseguir tambi´en que crezca a voluntad. . . pero siempre que no se rebase la capacidad del vector. Hemos representado las celdas a la derecha del terminador como cajas vac´ıas, pero no es cierto que lo est´en. Lo normal es que contengan valores arbitrarios, aunque eso no importa mucho: el convenio de que la cadena termina en el primer car´acter nulo hace que el resto de caracteres no se tenga en cuenta. Es posible que, en el ejemplo anterior, la memoria presente realmente este aspecto: a t 0 e 1 x 2 t 3 o 4 0 5 a 6 u 7 0 8 x 9 Por comodidad representaremos las celdas a la derecha del terminador con cajas vac´ıas, pues no importa en absoluto lo que contienen. ¿Qu´e ocurre si intentamos inicializar una zona de memoria reservada para s´olo 10 chars con una cadena de longitud mayor que 9? char a[10] = "supercalifragilisticoespialidoso"; // ! Mal! Estaremos cometiendo un grav´ısimo error de programaci´on que, posiblemente, no detecte el compilador. Los caracteres que no caben en a se escriben en la zona de memoria que sigue a la zona ocupada por a. a s 0 u 1 p 2 e 3 r 4 c 5 a 6 l 7 i 8 f 9 r a g i l i s t i c o e s p i a l i d o s o 0 Ya vimos en un apartado anterior las posibles consecuencias de ocupar memoria que no nos ha sido reservada: puede que modifiques el contenido de otras variables o que trates de escribir en una zona que te est´a vetada, con el consiguiente aborto de la ejecuci´on del programa. 92 Introducci´on a la Programaci´on con C
  • 99. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros Como resulta que en una variable con capacidad para, por ejemplo, 80 caracteres s´olo caben realmente 79 caracteres aparte del nulo, adoptaremos una curiosa pr´actica al declarar variables de cadena que nos permitir´a almacenar los 80 caracteres (adem´as del nulo) sin crear una constante confusi´on con respecto al n´umero de caracteres que caben en ellas: 1 #include <stdio.h> 2 3 #define MAXLON 80 4 5 int main(void) 6 { 7 char cadena[MAXLON+1]; /* Reservamos 81 caracteres: 80 caracteres m´as el terminador */ 8 9 return 0; 10 } 2.2.3. Entrada/salida de cadenas Las cadenas se muestran con printf y la adecuada marca de formato sin que se presenten dificultades especiales. Lo que s´ı resulta problem´atico es leer cadenas. La funci´on scanf presenta una seria limitaci´on: s´olo puede leer ((palabras)), no ((frases)). Ello nos obligar´a a presentar una nueva funci´on (gets). . . que se lleva fatal con scanf . Salida con printf Empecemos por considerar la funci´on printf , que muestra cadenas con la marca de formato %s. Aqu´ı tienes un ejemplo de uso: salida cadena.c salida cadena.c 1 #include <stdio.h> 2 3 #define MAXLON 80 4 5 int main(void) 6 { 7 char cadena[MAXLON+1] = "una cadena"; 8 9 printf ("El valor de cadena es %s.n", cadena); 10 11 return 0; 12 } Al ejecutar el programa obtienes en pantalla esto: El valor de cadena es una cadena. Puedes alterar la presentaci´on de la cadena con modificadores: salida cadena con modificadores.c salida cadena con modificadores.c 1 #include <stdio.h> 2 3 #define MAXLON 80 4 5 int main(void) 6 { 7 char cadena[MAXLON+1] = "una cadena"; 8 9 printf ("El valor de cadena es (%s).n", cadena); 10 printf ("El valor de cadena es (%20s).n", cadena); 11 printf ("El valor de cadena es (%-20s).n", cadena); 12 13 return 0; 14 } Introducci´on a la Programaci´on con C 93
  • 100. 2.2 Cadenas est´aticas El valor de cadena es (una cadena). El valor de cadena es ( una cadena). El valor de cadena es (una cadena ). ¿Y si deseamos mostrar una cadena car´acter a car´acter? Podemos hacerlo llamando a printf sobre cada uno de los caracteres, pero recuerda que la marca de formato asociada a un car´acter es %c: salida caracter a caracter.c salida caracter a caracter.c 1 #include <stdio.h> 2 3 #define MAXLON 80 4 5 int main(void) 6 { 7 char cadena[MAXLON+1] = "una cadena"; 8 int i; 9 10 i = 0; 11 while (cadena[i] != ’0’) { 12 printf ("%cn", cadena[i]); 13 i++; 14 } 15 16 return 0; 17 } Este es el resultado de la ejecuci´on: u n a c a d e n a Entrada con scanf Poco m´as hay que contar acerca de printf . La funci´on scanf es un reto mayor. He aqu´ı un ejemplo que pretende leer e imprimir una cadena en la que podemos guardar hasta 80 caracteres (sin contar el terminador nulo): lee una cadena.c lee una cadena.c 1 #include <stdio.h> 2 3 #define MAXLON 80 4 5 int main(void) 6 { 7 char cadena[MAXLON+1]; 8 9 scanf ("%s", cadena); 10 printf ("La cadena le´ıda es %sn", cadena); 11 12 return 0; 13 } ¡Ojo! ¡No hemos puesto el operador & delante de cadena! ¿Es un error? No. Con las cadenas no hay que poner el car´acter & del identificador al usar scanf . ¿Por qu´e? Porque scanf espera 94 Introducci´on a la Programaci´on con C
  • 101. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros una direcci´on de memoria y el identificador, por la dualidad vector-puntero, ¡es una direcci´on de memoria! Recuerda: cadena[0] es un char, pero cadena, sin m´as, es la direcci´on de memoria en la que empieza el vector de caracteres. Ejecutemos el programa e introduzcamos una palabra: una La cadena le´ıda es una . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 99 ¿Es v´alida esta otra forma de leer una cadena? Pru´ebala en tu ordenador. 1 #include <stdio.h> 2 3 #define MAXLON 80 4 5 int main(void) 6 { 7 char cadena[MAXLON+1]; 8 9 scanf ("%s", &cadena[0]); 10 printf ("La cadena le´ıda es %s.n", cadena); 11 12 return 0; 13 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Cuando scanf recibe el valor asociado a cadena, recibe una direcci´on de memoria y, a partir de ella, deja los caracteres le´ıdos de teclado. Debes tener en cuenta que si los caracteres le´ıdos exceden la capacidad de la cadena, se producir´a un error de ejecuci´on. ¿Y por qu´e printf no muestra por pantalla una simple direcci´on de memoria cuando ejecuta- mos la llamada printf ("La cadena le´ıda es %s.n", cadena)? Si es cierto lo dicho, cadena es una direcci´on de memoria. La explicaci´on es que la marca %s es interpretada por printf como ((me pasan una direcci´on de memoria en la que empieza una cadena, as´ı que he de mostrar su contenido car´acter a car´acter hasta encontrar un car´acter nulo)). Lectura con gets Hay un problema pr´actico con scanf : s´olo lee una ((palabra)), es decir, una secuencia de caracteres no blancos. Hagamos la prueba: lee frase mal.c E lee frase mal.c E 1 #include <stdio.h> 2 3 #define MAXLON 80 4 5 int main(void) 6 { 7 char cadena[MAXLON+1]; 8 9 scanf ("%s", cadena); 10 printf ("La cadena le´ıda es %s.n", cadena); 11 12 return 0; 13 } Si al ejecutar el programa tecleamos un par de palabras, s´olo se muestra la primera: una frase La cadena le´ıda es una. ¿Qu´e ha ocurrido con los restantes caracteres tecleados? ¡Est´an a la espera de ser le´ıdos! La siguiente cadena le´ıda, si hubiera un nuevo scanf , ser´ıa "frase". Si es lo que quer´ıamos, perfecto, pero si no, el desastre puede ser may´usculo. Introducci´on a la Programaci´on con C 95
  • 102. 2.2 Cadenas est´aticas ¿C´omo leer, pues, una frase completa? No hay forma sencilla de hacerlo con scanf . Tendre- mos que recurrir a una funci´on diferente. La funci´on gets lee todos los caracteres que hay hasta encontrar un salto de l´ınea. Dichos caracteres, excepto el salto de l´ınea, se almacenan a partir de la direcci´on de memoria que se indique como argumento y se a˜nade un terminador. Aqu´ı tienes un ejemplo: 1 #include <stdio.h> 2 3 #define MAXLON 11 4 5 int main(void) 6 { 7 char a[MAXLON+1], b[MAXLON+1]; 8 9 printf ("Introduce una cadena: "); gets(a); 10 printf ("Introduce otra cadena: "); gets(b); 11 printf ("La primera es %s y la segunda es %sn", a, b); 12 13 return 0; 14 } Ejecutemos el programa: Introduce una cadena: uno dos Introduce otra cadena: tres cuatro La primera es uno dos y la segunda es tres cuatro Overflow exploit El manejo de cadenas C es complicado. . . y peligroso. La posibilidad de que se almace- nen m´as caracteres de los que caben en una zona de memoria reservada para una cadena ha dado lugar a una t´ecnica de cracking muy com´un: el overflow exploit (que significa ((aprovechamiento del desbordamiento))), tambi´en conocido por smash the stack (((machacar la pila))). Si un programa C lee una cadena con scanf o gets es vulnerable a este tipo de ataques. La idea b´asica es la siguiente. Si c es una variable local a una funci´on (en el siguiente cap´ıtulo veremos c´omo), reside en una zona de memoria especial: la pila. Podemos desbordar la zona de memoria reservada para la cadena c escribiendo un texto m´as largo del que cabe en ella. Cuando eso ocurre, estamos ocupando memoria en una zona de la pila que no nos ((pertenece)). Podemos conseguir as´ı escribir informaci´on en una zona de la pila reservada a informaci´on como la direcci´on de retorno de la funci´on. El exploit se basa en asignar a la direcci´on de retorno el valor de una direcci´on en la que habremos escrito una rutina especial en c´odigo m´aquina. ¿Y c´omo conseguimos introducir una rutina en c´odigo m´aquina en un programa ajeno? ¡En la propia cadena que provoca el desbordamiento, codific´andola en binario! La rutina de c´odigo m´aquina suele ser sencilla: efect´ua una simple llamada al sistema operativo para que ejecute un int´erprete de ´ordenes Unix. El int´erprete se ejecutar´a con los mismos permisos que el programa que hemos reventado. Si el programa atacado se ejecutaba con permisos de root, habremos conseguido ejecutar un int´erprete de ´ordenes como root. ¡El ordenador es nuestro! ¿Y c´omo podemos proteger a nuestros programas de los overflow exploit? Pues, para empezar, no utilizando nunca scanf o gets directamente. Como es posible leer de teclado car´acter a car´acter (lo veremos en el cap´ıtulo dedicado a ficheros), podemos definir nuestra propia funci´on de lectura de cadenas: una funci´on de lectura que controle que nunca se escribe en una zona de memoria m´as informaci´on de la que cabe. Dado que gets es tan vulnerable a los overflow exploit, el compilador de C te dar´a un aviso cuando la uses. No te sorprendas, pues, cuando veas un mensaje como ´este: ((the ‘gets’ function is dangerous and should not be used)). Lectura de cadenas y escalares: gets y sscanf Y ahora, vamos con un problema al que te enfrentar´as en m´as de una ocasi´on: la lectura alterna de cadenas y valores escalares. La mezcla de llamadas a scanf y a gets, produce efectos 96 Introducci´on a la Programaci´on con C
  • 103. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros curiosos que se derivan de la combinaci´on de su diferente comportamiento frente a los blancos. El resultado suele ser una lectura incorrecta de los datos o incluso el bloqueo de la ejecuci´on del programa. Los detalles son bastante escabrosos. Si tienes curiosidad, te los mostramos en el apartado B.3. Presentaremos en este cap´ıtulo una soluci´on directa que deber´as aplicar siempre que tu programa alterne la lectura de cadenas con blancos y valores escalares (algo muy frecuente). La soluci´on consiste en: Si vas a leer una cadena usar gets. Y si vas a leer un valor escalar, proceder en dos pasos: • leer una l´ınea completa con gets (usa una variable auxiliar para ello), • y extraer de ella los valores escalares que se deseaba leer con ayuda de la funci´on sscanf . La funci´on sscanf es similar a scanf (f´ıjate en la ((s)) inicial), pero no obtiene informaci´on ley´endola del teclado, sino que la extrae de una cadena. Un ejemplo ayudar´a a entender el procedimiento: lecturas.c lecturas.c 1 #include <stdio.h> 2 3 #define MAXLINEA 80 4 #define MAXFRASE 40 5 6 int main(void) 7 { 8 int a, b; 9 char frase[MAXFRASE+1]; 10 char linea[MAXLINEA+1]; 11 12 printf ("Dame el valor de un entero:"); 13 gets(linea); sscanf (linea, "%d", &a); 14 15 printf ("Introduce ahora una frase:"); 16 gets(frase); 17 18 printf ("Y ahora, dame el valor de otro entero:"); 19 gets(linea); sscanf (linea, "%d", &b); 20 21 printf ("Enteros le´ıdos: %d, %d.n", a, b); 22 printf ("Frase le´ıda: %s.n", frase); 23 24 return 0; 25 } En el programa hemos definido una variable auxiliar, linea, que es una cadena con capacidad para 80 caracteres m´as el terminador (puede resultar conveniente reservar m´as memoria para ella en seg´un qu´e aplicaci´on). Cada vez que deseamos leer un valor escalar, leemos en linea un texto que introduce el usuario y obtenemos el valor escalar con la funci´on sscanf . Dicha funci´on recibe, como primer argumento, la cadena en linea; como segundo, una cadena con marcas de formato; y como tercer par´ametro, la direcci´on de la variable escalar en la que queremos depositar el resultado de la lectura. Es un proceso un tanto inc´omodo, pero al que tenemos que acostumbrarnos. . . de momento. 2.2.4. Asignaci´on y copia de cadenas Este programa, que pretende copiar una cadena en otra, parece correcto, pero no lo es: 1 #define MAXLON 10 2 3 int main(void) Introducci´on a la Programaci´on con C 97
  • 104. 2.2 Cadenas est´aticas 4 { 5 char original[MAXLON+1] = "cadena"; 6 char copia[MAXLON+1]; 7 8 copia = original; 9 10 return 0; 11 } Si compilas el programa, obtendr´as un error que te impedir´a obtener un ejecutable. Recuerda: los identificadores de vectores est´aticos se consideran punteros inmutables y, a fin de cuentas, las cadenas son vectores est´aticos (m´as adelante aprenderemos a usar vectores din´amicos). Para efectuar una copia de una cadena, has de hacerlo car´acter a car´acter. 1 #define MAXLON 10 2 3 int main(void) 4 { 5 char original[MAXLON+1] = "cadena"; 6 char copia[MAXLON+1]; 7 int i; 8 9 for (i = 0; i <= MAXLON; i++) 10 copia[i] = original[i]; 11 12 return 0; 13 } F´ıjate en que el bucle recorre los 10 caracteres que realmente hay en original pero, de hecho, s´olo necesitas copiar los caracteres que hay hasta el terminador, incluy´endole a ´el. 1 #define MAXLON 10 2 3 int main(void) 4 { 5 char original[MAXLON+1] = "cadena"; 6 char copia[MAXLON+1]; 7 int i; 8 9 for (i = 0; i <= MAXLON; i++) { 10 copia[i] = original[i]; 11 if (copia[i] == ’0’) 12 break; 13 } 14 15 return 0; 16 } original c 0 a 1 d 2 e 3 n 4 a 5 0 6 7 8 9 copia c 0 a 1 d 2 e 3 n 4 a 5 0 6 7 8 9 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 100 ¿Qu´e problema presenta esta otra versi´on del mismo programa? 1 #define MAXLON 10 2 3 int main(void) 4 { 5 char original[MAXLON+1] = "cadena"; 6 char copia[MAXLON+1]; 7 int i; 8 98 Introducci´on a la Programaci´on con C
  • 105. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros 9 for (i = 0; i <= MAXLON; i++) { 10 if (copia[i] == ’0’) 11 break; 12 else 13 copia[i] = original[i]; 14 } 15 16 return 0; 17 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A´un podemos hacerlo ((mejor)): 1 #define MAXLON 10 2 3 int main(void) 4 { 5 char original[MAXLON+1] = "cadena"; 6 char copia[MAXLON+1]; 7 int i; 8 9 for (i = 0; original[i] != ’0’; i++) { 10 copia[i] = original[i]; 11 copia[i] = ’0’; 12 13 return 0; 14 } ¿Ves? La condici´on del for controla si hemos llegado al terminador o no. Como el termina- dor no llega a copiarse, lo a˜nadimos tan pronto finaliza el bucle. Este tipo de bucles, aunque perfectamente legales, pueden resultar desconcertantes. Una versi´on m´as del copiado de cadenas Considera esta otra versi´on del copiado de cadenas: 1 #define MAXLON 10 2 3 int main(void) 4 { 5 char original[MAXLON+1] = "cadena"; 6 char copia[MAXLON+1]; 7 int i; 8 9 i = 0; 10 while ( (copia[i] = original[i++]) != ’0’) ; 11 12 return 0; 13 } El bucle est´a vac´ıo y la condici´on del bucle while es un tanto extra˜na. Se aprovecha de que la asignaci´on es una operaci´on que devuelve un valor, as´ı que lo puede comparar con el terminador. Y no s´olo eso: el avance de i se logra con un postincremento en el mism´ısimo acceso al elemento de original. Este tipo de retru´ecanos es muy habitual en los programas C. Y es discutible que as´ı sea: los programas que hacen este tipo de cosas no tienen por qu´e ser m´as r´apidos y resultan m´as dif´ıciles de entender (a menos que lleves mucho tiempo programando en C). Aqu´ı tienes una versi´on con una condici´on del bucle while diferente: i = 0; while (copia[i] = original[i++]) ; ¿Ves por qu´e funciona esta otra versi´on? El copiado de cadenas es una acci´on frecuente, as´ı que hay funciones predefinidas para ello, accesibles incluyendo la cabecera string.h: Introducci´on a la Programaci´on con C 99
  • 106. 2.2 Cadenas est´aticas 1 #include <string.h> 2 3 #define MAXLON 10 4 5 int main(void) 6 { 7 char original[MAXLON+1] = "cadena"; 8 char copia[MAXLON+1]; 9 10 strcpy(copia, original); // Copia el contenido de original en copia. 11 12 return 0; 13 } Ten cuidado: strcpy (abreviatura de ((string copy))) no comprueba si el destino de la copia tiene capacidad suficiente para la cadena, as´ı que puede provocar un desbordamiento. La funci´on strcpy se limita a copiar car´acter a car´acter hasta llegar a un car´acter nulo. Copias (m´as) seguras Hemos dicho que strcpy presenta un fallo de seguridad: no comprueba si el destino es capaz de albergar todos los caracteres de la cadena original. Si quieres asegurarte de no rebasar la capacidad del vector destino puedes usar strncpy, una versi´on de strcpy que copia la cadena, pero con un l´ımite al n´umero m´aximo de caracteres: 1 #include <string.h> 2 3 #define MAXLON 10 4 5 int main(void) 6 { 7 char original[MAXLON+1] = "cadena"; 8 char copia[MAXLON+1]; 9 10 strncpy(copia, original, MAXLON+1); // Copia, a lo sumo, MAXLON+1 caracteres. 11 12 return 0; 13 } Pero tampoco strncpy es perfecta. Si la cadena original tiene m´as caracteres de los que puede almacenar la cadena destino, la copia es imperfecta: no acabar´a en ’0’. De todos modos, puedes encargarte t´u mismo de terminar la cadena en el ´ultimo car´acter, por si acaso: 1 #include <string.h> 2 3 #define MAXLON 10 4 5 int main(void) 6 { 7 char original[MAXLON+1] = "cadena"; 8 char copia[MAXLON+1]; 9 10 strncpy(copia, original, MAXLON+1); 11 copia[MAXLON] = ’0’; 12 13 return 0; 14 } Tampoco est´a permitido asignar un literal de cadena a un vector de caracteres fuera de la zona de declaraci´on de variables. Es decir, este programa es incorrecto: 1 #define MAXLON 10 100 Introducci´on a la Programaci´on con C
  • 107. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros 2 3 int main(void) 4 { 5 char a[MAXLON+1]; 6 7 a = "cadena"; // ! Mal! 8 9 return 0; 10 } Si deseas asignar un literal de cadena, tendr´as que hacerlo con la ayuda de strcpy: 1 #include <string.h> 2 3 #define MAXLON 10 4 5 int main(void) 6 { 7 char a[MAXLON+1]; 8 9 strcpy(a, "cadena"); 10 11 return 0; 12 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 101 Dise˜na un programa que lea una cadena y copie en otra una versi´on encriptada. La encriptaci´on convertir´a cada letra (del alfabeto ingl´es) en la que le sigue en la tabla ASCII (excepto en el caso de las letras ((z)) y ((Z)), que ser´an sustituidas por ((a)) y ((A)), respectivamente.) No uses la funci´on strcpy. · 102 Dise˜na un programa que lea una cadena que posiblemente contenga letras may´usculas y copie en otra una versi´on de la misma cuyas letras sean todas min´usculas. No uses la funci´on strcpy. · 103 Dise˜na un programa que lea una cadena que posiblemente contenga letras may´usculas y copie en otra una versi´on de la misma cuyas letras sean todas min´usculas. Usa la funci´on strcpy para obtener un duplicado de la cadena y, despu´es, recorre la copia para ir sustituyendo en ella las letras may´usculas por sus correspondientes min´usculas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.5. Longitud de una cadena El convenio de terminar una cadena con el car´acter nulo permite conocer f´acilmente la longitud de una cadena: 1 #include <stdio.h> 2 3 #define MAXLON 80 4 5 int main(void) 6 { 7 char a[MAXLON+1]; 8 int i; 9 10 printf ("Introduce una cadena (m´ax. %d cars.): ", MAXLON); 11 gets(a); 12 i = 0; 13 while (a[i] != ’0’) 14 i++; 15 printf ("Longitud de la cadena: %dn", i); 16 17 return 0; 18 } Introducci´on a la Programaci´on con C 101
  • 108. 2.2 Cadenas est´aticas El estilo C El programa que hemos presentado para calcular la longitud de una cadena es un programa C correcto, pero no es as´ı como un programador C expresar´ıa esa misma idea. ¡No hace falta que el bucle incluya sentencia alguna!: 1 #include <stdio.h> 2 3 #define MAXLON 80 4 5 int main(void) 6 { 7 char a[MAXLON+1]; 8 int i; 9 10 printf ("Introduce una cadena (m´ax. %d cars.): ", MAXLON); 11 gets(a); 12 i = 0; 13 while (a[i++] != ’0’) ; // Observa que no hay sentencia alguna en el while. 14 printf ("Longitud de la cadena: %dn", i-1); 15 16 return 0; 17 } El operador de postincremento permite aumentar en uno el valor de i justo despu´es de consultar el valor de a[i]. Eso s´ı, hemos tenido que modificar el valor mostrado como longitud, pues ahora i acaba valiendo uno m´as. Es m´as, ni siquiera es necesario efectuar comparaci´on alguna. El bucle se puede sustituir por este otro: i = 0; while (a[i++]) ; El bucle funciona correctamente porque el valor ’0’ significa ((falso)) cuando se interpreta como valor l´ogico. El bucle itera, pues, hasta llegar a un valor falso, es decir, a un terminador. Algunos problemas con el operador de autoincremento ¿Qu´e esperamos que resulte de ejecutar esta sentencia? 1 int a[5] = {0, 0, 0, 0, 0}; 2 3 i = 1; 4 a[i] = i++; Hay dos posibles interpretaciones: Se eval´ua primero la parte derecha de la asignaci´on, as´ı que i pasa a valer 2 y se asigna ese valor en a[2]. Se eval´ua primero la asignaci´on, con lo que se asigna el valor 1 en a[1] y, despu´es, se incrementa el valor de i, que pasa a valer 2. ¿Qu´e hace C? No se sabe. La especificaci´on del lenguaje est´andar indica que el resultado est´a indefinido. Cada compilador elige qu´e hacer, as´ı que ese tipo de sentencias pueden dar problemas de portabilidad. Conviene, pues, evitarlas. Calcular la longitud de una cadena es una operaci´on frecuentemente utilizada, as´ı que est´a predefinida en la biblioteca de tratamiento de cadenas. Si inclu´ımos la cabecera string.h, podemos usar la funci´on strlen (abreviatura de ((string length))): 1 #include <stdio.h> 2 #include <string.h> 3 102 Introducci´on a la Programaci´on con C
  • 109. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros while o for Los bucles while pueden sustituirse muchas veces por bucles for equivalentes, bastante m´as compactos: 1 #include <stdio.h> 2 3 #define MAXLON 80 4 5 int main(void) 6 { 7 char a[MAXLON+1]; 8 int i; 9 10 printf ("Introduce una cadena (m´ax. %d cars.): ", MAXLON); 11 gets(a); 12 for (i=0; a[i] != ’0’; i++) ; // Tampoco hay sentencia alguna en el for. 13 printf ("Longitud de la cadena: %dn", i); 14 15 return 0; 16 } Tambi´en aqu´ı es superflua la comparaci´on: for (i=0; a[i]; i++) ; Todas las versiones del programa que hemos presentado son equivalentes. Escoger una u otra es cuesti´on de estilo. 4 #define MAXLON 80 5 6 int main(void) 7 { 8 char a[MAXLON+1]; 9 int l; 10 11 printf ("Introduce una cadena (m´ax. %d cars.): ", MAXLON); 12 gets(a); 13 l = strlen(a); 14 printf ("Longitud de la cadena: %dn", l); 15 16 return 0; 17 } Has de ser consciente de qu´e hace strlen: lo mismo que hac´ıa el primer programa, es decir, recorrer la cadena de izquierda a derecha incrementando un contador hasta llegar al terminador nulo. Esto implica que tarde tanto m´as cuanto m´as larga sea la cadena. Has de estar al tanto, pues, de la fuente de ineficiencia que puede suponer utilizar directamente strlen en lugares cr´ıticos como los bucles. Por ejemplo, esta funci´on cuenta las vocales min´usculas de una cadena le´ıda por teclado: 1 #include <stdio.h> 2 #include <string.h> 3 4 #define MAXLON 80 5 6 int main(void) 7 { 8 char a[MAXLON+1]; 9 int i, contador; 10 11 printf ("Introduce una cadena (m´ax. %d cars.): ", MAXLON); 12 gets(a); 13 contador = 0; Introducci´on a la Programaci´on con C 103
  • 110. 2.2 Cadenas est´aticas 14 for (i = 0; i < strlen(a); i++) 15 if (a[i] == ’a’ || a[i] == ’e’ || a[i] == ’i’ || a[i] == ’o’ || a[i] == ’u’) 16 contador++; 17 printf ("Vocales min´usculas: %dn", contador); 18 19 return 0; 20 } Pero tiene un problema de eficiencia. Con cada iteraci´on del bucle for se llama a strlen y strlen tarda un tiempo proporcional a la longitud de la cadena. Si la cadena tiene, pongamos, 60 caracteres, se llamar´a a strlen 60 veces para efectuar la comparaci´on, y para cada llamada, strlen tardar´a unos 60 pasos en devolver lo mismo: el valor 60. Esta nueva versi´on del mismo programa no presenta ese inconveniente: 1 #include <stdio.h> 2 #include <string.h> 3 4 #define MAXLON 80 5 6 int main(void) 7 { 8 char a[MAXLON+1]; 9 int i, longitud , contador; 10 11 printf ("Introduce una cadena (m´ax. %d cars.): ", MAXLON); 12 gets(a); 13 longitud = strlen(cadena); 14 contador = 0; 15 for (i = 0; i < longitud ; i++) 16 if (a[i] == ’a’ || a[i] == ’e’ || a[i] == ’i’ || a[i] == ’o’ || a[i] == ’u’) 17 contador++; 18 printf ("Vocales min´usculas: %dn", contador); 19 20 return 0; 21 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 104 Dise˜na un programa que lea una cadena y la invierta. · 105 Dise˜na un programa que lea una palabra y determine si es o no es pal´ındromo. · 106 Dise˜na un programa que lea una frase y determine si es o no es pal´ındromo. Recuerda que los espacios en blanco y los signos de puntuaci´on no se deben tener en cuenta a la hora de determinar si la frase es pal´ındromo. · 107 Escribe un programa C que lea dos cadenas y muestre el ´ındice del car´acter de la primera cadena en el que empieza, por primera vez, la segunda cadena. Si la segunda cadena no est´a contenida en la primera, el programa nos lo har´a saber. (Ejemplo: si la primera cadena es "un ejercicio de ejemplo" y la segunda es "eje", el programa mostrar´a el valor 3.) · 108 Escribe un programa C que lea dos cadenas y muestre el ´ındice del car´acter de la primera cadena en el que empieza por ´ultima vez una aparici´on de la segunda cadena. Si la segunda cadena no est´a contenida en la primera, el programa nos lo har´a saber. (Ejemplo: si la primera cadena es "un ejercicio de ejemplo" y la segunda es "eje", el programa mostrar´a el valor 16.) · 109 Escribe un programa que lea una l´ınea y haga una copia de ella eliminando los espacios en blanco que haya al principio y al final de la misma. · 110 Escribe un programa que lea repetidamente l´ıneas con el nombre completo de una persona. Para cada persona, guardar´a temporalmente en una cadena sus iniciales (las letras con may´usculas) separadas por puntos y espacios en blanco y mostrar´a el resultado en pantalla. El programa finalizar´a cuando el usuario escriba una l´ınea en blanco. 104 Introducci´on a la Programaci´on con C
  • 111. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros · 111 Dise˜na un programa C que lea un entero n y una cadena a y muestre por pantalla el valor (en base 10) de la cadena a si se interpreta como un n´umero en base n. El valor de n debe estar comprendido entre 2 y 16. Si la cadena a contiene un car´acter que no corresponde a un d´ıgito en base n, notificar´a el error y no efectuar´a c´alculo alguno. Ejemplos: si a es "ff" y n es 16, se mostrar´a el valor 255; si a es "f0" y n es 15, se notificar´a un error: ((f no es un d´ıgito en base 15)); si a es "1111" y n es 2, se mostrar´a el valor 15. · 112 Dise˜na un programa C que lea una l´ınea y muestre por pantalla el n´umero de palabras que hay en ella. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.6. Concatenaci´on Python permit´ıa concatenar cadenas con el operador +. En C no puedes usar + para concatenar cadenas. Una posibilidad es que las concatenes t´u mismo ((a mano)), con bucles. Este programa, por ejemplo, pide dos cadenas y concatena la segunda a la primera: 1 #include <stdio.h> 2 3 #define MAXLON 80 4 5 int main(void) 6 { 7 char a[MAXLON+1], b[MAXLON+1]; 8 int longa, longb; 9 int i; 10 11 printf ("Introduce un texto (m´ax. %d cars.): ", MAXLON); gets(a); 12 printf ("Introduce otro texto (m´ax. %d cars.): ", MAXLON); gets(b); 13 14 longa = strlen(a); 15 longb = strlen(b); 16 for (i=0; i<longb; i++) 17 a[longa+i] = b[i]; 18 a[longa+longb] = ’0’; 19 printf ("Concatenaci´on de ambos: %s", a); 20 21 return 0; 22 } Pero es mejor usar la funci´on de librer´ıa strcat (por ((string concatenate))): 1 #include <stdio.h> 2 #include <string.h> 3 4 #define MAXLON 80 5 6 int main(void) 7 { 8 char a[MAXLON+1], b[MAXLON+1]; 9 10 printf ("Introduce un texto (m´ax. %d cars.): ", MAXLON); 11 gets(a); 12 printf ("Introduce otro texto (m´ax. %d cars.): ", MAXLON); 13 gets(b); 14 strcat(a, b); // Equivale a la asignaci´on Python a = a + b 15 printf ("Concatenaci´on de ambos: %s", a); 16 17 return 0; 18 } Introducci´on a la Programaci´on con C 105
  • 112. 2.2 Cadenas est´aticas Si quieres dejar el resultado de la concatenaci´on en una variable distinta, deber´as actuar en dos pasos: 1 #include <stdio.h> 2 #include <string.h> 3 4 #define MAXLON 80 5 6 int main(void) 7 { 8 char a[MAXLON+1], b[MAXLON+1], c[MAXLON+1]; 9 10 printf ("Introduce un texto (m´ax. %d cars.): ", MAXLON); 11 gets(a); 12 printf ("Introduce otro texto (m´ax. %d cars.): ", MAXLON); 13 gets(b); 14 strcpy(c, a); // ´Esta seguida de... 15 strcat(c, b); // ... ´esta equivale a la sentencia Python c = a + b 16 printf ("Concatenaci´on de ambos: %s", c); 17 18 return 0; 19 } Recuerda que es responsabilidad del programador asegurarse de que la cadena que recibe la concatenaci´on dispone de capacidad suficiente para almacenar la cadena resultante. Por cierto, el operador de repetici´on de cadenas que encontr´abamos en Python (operador *) no est´a disponible en C ni hay funci´on predefinida que lo proporcione. Un car´acter no es una cadena Un error frecuente es intentar a˜nadir un car´acter a una cadena con strcat o asign´arselo como ´unico car´acter con strcpy: char linea[10] = "cadena"; char caracter = ’s’; strcat(linea, caracter); // ! Mal! strcpy(linea, ’x’); // ! Mal! Recuerda: los dos datos de strcat y strcpy han de ser cadenas y no es aceptable que uno de ellos sea un car´acter. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 113 Escribe un programa C que lea el nombre y los dos apellidos de una persona en tres cadenas. A continuaci´on, el programa formar´a una s´ola cadena en la que aparezcan el nombre y los apellidos separados por espacios en blanco. · 114 Escribe un programa C que lea un verbo regular de la primera conjugaci´on y lo mues- tre por pantalla conjugado en presente de indicativo. Por ejemplo, si lee el texto programar, mostrar´a por pantalla: yo programo t´u programas ´el programa nosotros programamos vosotros program´ais ellos programan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.7. Comparaci´on de cadenas Tampoco los operadores de comparaci´on (==, !=, <, <=, >, >=) funcionan con cadenas. Existe, no obstante, una funci´on de string.h que permite paliar esta carencia de C: strcmp (abreviatura 106 Introducci´on a la Programaci´on con C
  • 113. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros de ((string comparison))). La funci´on strcmp recibe dos cadenas, a y b, y devuelve un entero. El entero que resulta de efectuar la llamada strcmp(a, b) codifica el resultado de la comparaci´on: es menor que cero si la cadena a es menor que b, es 0 si la cadena a es igual que b, y es mayor que cero si la cadena a es mayor que b. Naturalmente, menor significa que va delante en orden alfab´etico, y mayor que va detr´as. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 115 Dise˜na un programa C que lea dos cadenas y, si la primera es menor o igual que la segunda, imprima el texto ((menor o igual)). · 116 ¿Qu´e valor devolver´a la llamada strcmp("21", "112")? · 117 Escribe un programa que lea dos cadenas, a y b (con capacidad para 80 caracteres), y muestre por pantalla −1 si a es menor que b, 0 si a es igual que b, y 1 si a es mayor que b. Est´a prohibido que utilices la funci´on strcmp. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.8. Funciones ´utiles para manejar caracteres No s´olo string.h contiene funciones ´utiles para el tratamiento de cadenas. En ctype.h en- contrar´as unas funciones que permiten hacer c´omodamente preguntas acerca de los caracteres, como si son may´usculas, min´usculas, d´ıgitos, etc: isalnum(car´acter): devuelve cierto (un entero cualquiera distinto de cero) si car´acter es una letra o d´ıgito, y falso (el valor entero 0) en caso contrario, isalpha(car´acter): devuelve cierto si car´acter es una letra, y falso en caso contrario, isblank(car´acter): devuelve cierto si car´acter es un espacio en blanco o un tabulador, isdigit(car´acter) devuelve cierto si car´acter es un d´ıgito, y falso en caso contrario, isspace(car´acter): devuelve cierto si car´acter es un espacio en blanco, un salto de l´ınea, un retorno de carro, un tabulador, etc., y falso en caso contrario, islower(car´acter): devuelve cierto si car´acter es una letra min´uscula, y falso en caso contrario, isupper(car´acter): devuelve cierto si car´acter es una letra may´uscula, y falso en caso contrario. Tambi´en en ctype.h encontrar´as un par de funciones ´utiles para convertir caracteres de min´uscula a may´uscula y viceversa: toupper(car´acter): devuelve la may´uscula asociada a car´acter, si la tiene; si no, devuelve el mismo car´acter, tolower(car´acter): devuelve la min´uscula asociada a car´acter, si la tiene; si no, devuelve el mismo car´acter. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 118 ¿Qu´e problema presenta este programa? 1 #include <stdio.h> 2 #include <ctype.h> 3 4 int main(void) 5 { 6 char b[2] = "a"; 7 8 if (isalpha(b)) Introducci´on a la Programaci´on con C 107
  • 114. 2.2 Cadenas est´aticas 9 printf ("Es una letran"); 10 else 11 printf ("No es una letran"); 12 13 return 0; 14 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.9. Escritura en cadenas: sprintf Hay una funci´on que puede simplificar notablemente la creaci´on de cadenas cuyo contenido se debe calcular a partir de uno o m´as valores: sprintf , disponible incluyendo la cabecera stdio.h (se trata, en cierto modo, de la operaci´on complementaria de sscanf ). La funci´on sprintf se comporta como printf , salvo por un ((detalle)): no escribe texto en pantalla, sino que lo almacena en una cadena. F´ıjate en este ejemplo: 1 #include <stdio.h> 2 3 #define MAXLON 80 4 5 int main(void) 6 { 7 char a[MAXLON+1] = "una"; 8 char b[MAXLON+1] = "cadena"; 9 char c[MAXLON+1]; 10 11 sprintf (c, "%s %s", a, b); 12 printf ("%sn", c); 13 14 return 0; 15 } Si ejecutas el programa aparecer´a lo siguiente en pantalla: una cadena Como puedes ver, se ha asignado a c el valor de a seguido de un espacio en blanco y de la cadena b. Podr´ıamos haber conseguido el mismo efecto con llamadas a strcpy(c, a), strcat(c, " ") y strcat(c, b), pero sprintf resulta m´as legible y no cuesta mucho aprender a usarla, pues ya sabemos usar printf . No olvides que t´u eres responsable de que la informaci´on que se almacena en c quepa. En Python hay una acci´on an´aloga al sprintf de C: la asignaci´on a una variable de una cadena formada con el operador de formato. El mismo programa se podr´ıa haber escrito en Python as´ı: 1 # Ojo: programa Python 2 a = ’una’ 3 b = ’cadena’ 4 c = ’%s %s’ % (a, b) # Operaci´on an´aloga a sprintf en C. 5 print c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 119 ¿Qu´e almacena en la cadena a la siguiente sentencia? sprintf (a, "%d-%c-%d %s", 1, 48, 2, "si"); · 120 Escribe un programa que pida el nombre y los dos apellidos de una persona. Cada uno de esos tres datos debe almacenarse en una variable independiente. A continuaci´on, el programa crear´a y mostrar´a una nueva cadena con los dos apellidos y el nombre (separado de los apellidos por una coma). Por ejemplo, Juan P´erez L´opez dar´a lugar a la cadena "P´erez L´opez, Juan". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 Introducci´on a la Programaci´on con C
  • 115. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros 2.2.10. Un programa de ejemplo Vamos a implementar un programa que lee por teclado una l´ınea de texto y muestra por pantalla una cadena en la que las secuencias de blancos de la cadena original (espacios en blanco, tabuladores, etc.) se han sustituido por un s´olo espacio en blanco. Si, por ejemplo, el programa lee la cadena "una cadena con blancos ", mostrar´a por pantalla la cadena ((normalizada)) "una cadena con blancos ". normaliza.c normaliza.c 1 #include <stdio.h> 2 #include <string.h> 3 #include <ctype.h> 4 5 #define MAXLON 80 6 7 int main(void) 8 { 9 char a[MAXLON+1], b[MAXLON+1]; 10 int longitud, i, j; 11 12 printf ("Introduce una cadena (m´ax. %d cars.): ", MAXLON); 13 gets(a); 14 longitud = strlen(a); 15 b[0] = a[0]; 16 j = 1; 17 for (i=1; i<longitud; i++) 18 if (!isspace(a[i]) || (isspace(a[i]) && !isspace(a[i-1]))) 19 b[j++] = a[i]; 20 b[j] = ’0’; 21 printf ("La cadena normalizada es %sn", b); 22 23 return 0; 24 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 121 Modifica normaliza.c para que elimine, si los hay, los blancos inicial y final de la cadena normalizada. · 122 Haz un programa que lea una frase y construya una cadena que s´olo contenga sus letras min´usculas o may´usculas en el mismo orden con que aparecen en la frase. · 123 Haz un programa que lea una frase y construya una cadena que s´olo contenga sus letras min´usculas o may´usculas en el mismo orden con que aparecen en la frase, pero sin repetir ninguna. · 124 Lee un texto por teclado (con un m´aximo de 1000 caracteres) y muestra por pantalla la frecuencia de aparici´on de cada una de las letras del alfabeto (considera ´unicamente letras del alfabeto ingl´es), sin distinguir entre letras may´usculas y min´usculas (una aparici´on de la letra e y otra de la letra E cuentan como dos ocurrencias de la letra e). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3. Vectores multidimensionales Podemos declarar vectores de m´as de una dimensi´on muy f´acilmente: int a[10][5]; float b[3][2][4]; En este ejemplo, a es una matriz de 10 × 5 enteros y b es un vector de tres dimensiones con 3 × 2 × 4 n´umeros en coma flotante. Puedes acceder a un elemento cualquiera de los vectores a o b utilizando tantos ´ındices como dimensiones tiene el vector: a[4][2] y b[1][0][3], por ejemplo, son elementos de a y b, respectivamente. Introducci´on a la Programaci´on con C 109
  • 116. 2.3 Vectores multidimensionales La inicializaci´on de los vectores multidimensionales necesita tantos bucles anidados como dimensiones tengan ´estos: 1 int main(void) 2 { 3 int a[10][5]; 4 float b[3][2][4]; 5 int i, j, k; 6 7 for (i=0; i<10; i++) 8 for (j=0; j<5; j++) 9 a[i][j] = 0; 10 11 for (i=0; i<3; i++) 12 for (j=0; j<2; j++) 13 for (k=0; k<4; k++) 14 b[i][j][k] = 0.0; 15 16 return 0; 17 } Tambi´en puedes inicializar expl´ıcitamente un vector multidimensional: int c[3][3] = { {1, 0, 0}, {0, 1, 0}, {0, 0, 1} }; 2.3.1. Sobre la disposici´on de los vectores multidimensionales en memoria Cuando el compilador de C detecta la declaraci´on de un vector multidimensional, reserva tantas posiciones contiguas de memoria como sea preciso para albergar todas sus celdas. Por ejemplo, ante la declaraci´on int a[3][3], C reserva 9 celdas de 4 bytes, es decir, 36 bytes. He aqu´ı c´omo se disponen las celdas en memoria, suponiendo que la zona de memoria asignada empieza en la direcci´on 1000: 996: a[0][0]1000: a[0][1]1004: a[0][2]1008: a[1][0]1012: a[1][1]1016: a[1][2]1020: a[2][0]1024: a[2][1]1028: a[2][2]1032: 1036: Cuando accedemos a un elemento a[i][j], C sabe a qu´e celda de memoria acceder sumando a la direcci´on de a el valor (i*3+j)*4 (el 4 es el tama˜no de un int y el 3 es el n´umero de columnas). Aun siendo conscientes de c´omo representa C la memoria, nosotros trabajaremos con una representaci´on de una matriz de 3 × 3 como ´esta: a 0 1 2 0 1 2 Como puedes ver, lo relevante es que a es asimilable a un puntero a la zona de memoria en la que est´an dispuestos los elementos de la matriz. 110 Introducci´on a la Programaci´on con C
  • 117. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 125 Este programa es incorrecto. ¿Por qu´e? Aun siendo incorrecto, produce cierta salida por pantalla. ¿Qu´e muestra? matriz mal.c matriz mal.c 1 #include <stdio.h> 2 3 #define TALLA 3 4 5 int main(void) 6 { 7 int a[TALLA][TALLA]; 8 int i, j; 9 10 for (i=0; i<TALLA; i++) 11 for (j=0; j<TALLA; j++) 12 a[i][j] = 10*i+j; 13 14 for (j=0; j<TALLA*TALLA; j++) 15 printf ("%dn", a[0][j]); 16 17 return 0; 18 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.2. Un ejemplo: c´alculo matricial Para ilustrar el manejo de vectores multidimensionales construiremos ahora un programa que lee de teclado dos matrices de n´umeros en coma flotante y muestra por pantalla su suma y su producto. Las matrices le´ıdas ser´an de 3 × 3 y se denominar´an a y b. El resultado de la suma se almacenar´a en una matriz s y el del producto en otra p. Aqu´ı tienes el programa completo: matrices.c matrices.c 1 #include <stdio.h> 2 3 #define TALLA 3 4 5 int main(void) 6 { 7 float a[TALLA][TALLA], b[TALLA][TALLA]; 8 float s[TALLA][TALLA], p[TALLA][TALLA]; 9 int i, j, k; 10 11 /* Lectura de la matriz a */ 12 for (i=0; i<TALLA; i++) 13 for (j=0; j<TALLA; j++) { 14 printf ("Elemento (%d, %d): ", i, j); scanf ("%f", &a[i][j]); 15 } 16 17 /* Lectura de la matriz b */ 18 for (i=0; i<TALLA; i++) 19 for (j=0; j<TALLA; j++) { 20 printf ("Elemento (%d, %d): ", i, j); scanf ("%f", &b[i][j]); 21 } 22 23 /* C´alculo de la suma */ 24 for (i=0; i<TALLA; i++) 25 for (j=0; j<TALLA; j++) 26 s[i][j] = a[i][j] + b[i][j]; 27 28 /* C´alculo del producto */ Introducci´on a la Programaci´on con C 111
  • 118. 2.3 Vectores multidimensionales 29 for (i=0; i<TALLA; i++) 30 for (j=0; j<TALLA; j++) { 31 p[i][j] = 0.0; 32 for (k=0; k<TALLA; k++) 33 p[i][j] += a[i][k] * b[k][j]; 34 } 35 36 /* Impresi´on del resultado de la suma */ 37 printf ("Suman"); 38 for (i=0; i<TALLA; i++) { 39 for (j=0; j<TALLA; j++) 40 printf ("%8.3f", s[i][j]); 41 printf ("n"); 42 } 43 44 /* Impresi´on del resultado del producto */ 45 printf ("Producton"); 46 for (i=0; i<TALLA; i++) { 47 for (j=0; j<TALLA; j++) 48 printf ("%8.3f", p[i][j]); 49 printf ("n"); 50 } 51 52 return 0; 53 } A´un no sabemos definir nuestras propias funciones. En el pr´oximo cap´ıtulo volveremos a ver este programa y lo modificaremos para que use funciones definidas por nosotros. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 126 En una estaci´on meteorol´ogica registramos la temperatura (en grados cent´ıgrados) cada hora durante una semana. Almacenamos el resultado en una matriz de 7 × 24 (cada fila de la matriz contiene las 24 mediciones de un d´ıa). Dise˜na un programa que lea los datos por teclado y muestre: La m´axima y m´ınima temperaturas de la semana. La m´axima y m´ınima temperaturas de cada d´ıa. La temperatura media de la semana. La temperatura media de cada d´ıa. El n´umero de d´ıas en los que la temperatura media fue superior a 30 grados. · 127 Representamos diez ciudades con n´umeros del 0 al 9. Cuando hay carretera que une directamente a dos ciudades i y j, almacenamos su distancia en kil´ometros en la celda d[i][j] de una matriz de 10×10 enteros. Si no hay carretera entre ambas ciudades, el valor almacenado en su celda de d es cero. Nos suministran un vector en el que se describe un trayecto que pasa por las 10 ciudades. Determina si se trata de un trayecto v´alido (las dos ciudades de todo par consecutivo est´an unidas por un tramo de carretera) y, en tal caso, devuelve el n´umero de kil´ometros del trayecto. Si el trayecto no es v´alido, ind´ıcalo con un mensaje por pantalla. La matriz de distancias deber´as inicializarla expl´ıcitamente al declararla. El vector con el recorrido de ciudades deber´as leerlo de teclado. · 128 Dise˜na un programa que lea los elementos de una matriz de 4 × 5 flotantes y genere un vector de talla 4 en el que cada elemento contenga el sumatorio de los elementos de cada fila. El programa debe mostrar la matriz original y el vector en este formato (evidentemente, los valores deben ser los que correspondan a lo introducido por el usuario): 0 1 2 3 4 Suma 0 [ +27.33 +22.22 +10.00 +0.00 -22.22] -> +37.33 1 [ +5.00 +0.00 -1.50 +2.50 +10.00] -> +16.00 2 [ +3.45 +2.33 -4.56 +12.56 +12.01] -> +25.79 3 [ +1.02 +2.22 +12.70 +34.00 +12.00] -> +61.94 112 Introducci´on a la Programaci´on con C
  • 119. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . El programa que hemos presentado adolece de un serio inconveniente si nuestro objetivo era construir un programa ((general)) para multiplicar matrices: s´olo puede trabajar con matrices de TALLA×TALLA, o sea, de 3×3. ¿Y si quisi´eramos trabajar con matrices de tama˜nos arbitrarios? El primer problema al que nos enfrentar´ıamos es el de que las matrices han de tener una talla m´axima: no podemos, con lo que sabemos por ahora, reservar un espacio de memoria para las matrices que dependa de datos que nos suministra el usuario en tiempo de ejecuci´on. Usaremos, pues, una constante MAXTALLA con un valor razonablemente grande: pongamos 10. Ello permitir´a trabajar con matrices con un n´umero de filas y columnas menor o igual que 10, aunque ser´a a costa de malgastar memoria. matrices.c 1 #include <stdio.h> 2 3 #define MAXTALLA 10 4 5 int main(void) 6 { 7 float a[MAXTALLA][MAXTALLA], b[MAXTALLA][MAXTALLA]; 8 float s[MAXTALLA][MAXTALLA], p[MAXTALLA][MAXTALLA]; 9 ... El n´umero de filas y columnas de a se pedir´a al usuario y se almacenar´a en sendas variables: filas_a y columnas_a. Este gr´afico ilustra su papel: la matriz a es de 10 × 10, pero s´olo usamos una parte de ella (la zona sombreada) y podemos determinar qu´e zona es porque filas_a y columnas_a nos se˜nalan hasta qu´e fila y columna llega la zona ´util: 3columnas a a 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 5filas a Lo mismo se aplicar´a al n´umero de filas y columnas de b. Te mostramos el programa hasta el punto en que leemos la matriz a: matrices.c 1 #include <stdio.h> 2 3 #define MAXTALLA 10 4 5 int main(void) 6 { 7 float a[MAXTALLA][MAXTALLA], b[MAXTALLA][MAXTALLA]; 8 float s[MAXTALLA][MAXTALLA], p[MAXTALLA][MAXTALLA]; 9 int filas_a, columnas_a, filas_b, columnas_b ; 10 int i, j, k; 11 12 /* Lectura de la matriz a */ 13 printf ("Filas de a : "); scanf ("%d", &filas_a ); Introducci´on a la Programaci´on con C 113
  • 120. 2.3 Vectores multidimensionales 14 printf ("Columnas de a: "); scanf ("%d", &columnas_a ); 15 16 for (i=0; i<filas_a ; i++) 17 for (j=0; j<columnas_a ; j++) { 18 printf ("Elemento (%d, %d): ", i, j); scanf ("%f", &a[i][j]); 19 } 20 ... (Enc´argate t´u mismo de la lectura de b.) La suma s´olo es factible si filas a es igual a filas b y columnas a es igual a columnas b. matrices.c 1 #include <stdio.h> 2 3 #define MAXTALLA 10 4 5 int main(void) 6 { 7 float a[MAXTALLA][MAXTALLA], b[MAXTALLA][MAXTALLA]; 8 float s[MAXTALLA][MAXTALLA], p[MAXTALLA][MAXTALLA]; 9 int filas_a, columnas_a, filas_b, columnas_b; 10 int filas_s, columnas_s ; 11 int i, j, k; 12 13 /* Lectura de la matriz a */ 14 printf ("Filas de a : "); scanf ("%d", &filas_a); 15 printf ("Columnas de a: "); scanf ("%d", &columnas_a); 16 for (i=0; i<filas_a; i++) 17 for (j=0; j<columnas_a; j++) { 18 printf ("Elemento (%d, %d): ", i, j); scanf ("%f", &a[i][j]); 19 } 20 21 /* Lectura de la matriz b */ 22 ... 23 24 /* C´alculo de la suma */ 25 if (filas_a == filas_b && columnas_a == columnas_b) { 26 filas_s = filas_a; 27 columnas_s = columnas_a; 28 for (i=0; i<filas_s; i++) 29 for (j=0; j<filas_s; j++) 30 s[i][j] = a[i][j] + b[i][j]; 31 } 32 33 /* Impresi´on del resultado de la suma */ 34 if (filas_a == filas_b && columnas_a == columnas_b) { 35 printf ("Suman"); 36 for (i=0; i<filas_s; i++) { 37 for (j=0; j<columnas_s; j++) 38 printf ("%8.3f", s[i][j]); 39 printf ("n"); 40 } 41 } 42 else 43 printf ("Matrices no compatibles para la suma.n"); 44 45 ... Recuerda que una matriz de n × m elementos se puede multiplicar por otra de n × m elementos s´olo si m es igual a n (o sea, el n´umero de columnas de la primera es igual al de filas de la segunda) y que la matriz resultante es de dimensi´on n × m . matrices 1.c matrices.c 1 #include <stdio.h> 114 Introducci´on a la Programaci´on con C
  • 121. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros 2 3 #define MAXTALLA 10 4 5 int main(void) 6 { 7 float a[MAXTALLA][MAXTALLA], b[MAXTALLA][MAXTALLA]; 8 float s[MAXTALLA][MAXTALLA], p[MAXTALLA][MAXTALLA]; 9 int filas_a, columnas_a, filas_b, columnas_b; 10 int filas_s, columnas_s, filas_p, columnas_p ; 11 int i, j, k; 12 13 /* Lectura de la matriz a */ 14 printf ("Filas de a : "); scanf ("%d", &filas_a); 15 printf ("Columnas de a: "); scanf ("%d", &columnas_a); 16 for (i=0; i<filas_a; i++) 17 for (j=0; j<columnas_a; j++) { 18 printf ("Elemento (%d, %d): ", i, j); scanf ("%f", &a[i][j]); 19 } 20 21 /* Lectura de la matriz b */ 22 printf ("Filas de b : "); scanf ("%d", &filas_b); 23 printf ("Columnas de b: "); scanf ("%d", &columnas_b); 24 for (i=0; i<filas_b; i++) 25 for (j=0; j<columnas_b; j++) { 26 printf ("Elemento (%d, %d): ", i, j); scanf ("%f", &b[i][j]); 27 } 28 29 /* C´alculo de la suma */ 30 if (filas_a == filas_b && columnas_a == columnas_b) { 31 filas_s = filas_a; 32 columnas_s = columnas_a; 33 for (i=0; i<filas_s; i++) 34 for (j=0; j<filas_s; j++) 35 s[i][j] = a[i][j] + b[i][j]; 36 } 37 38 /* C´alculo del producto */ 39 if (columnas_a == filas_b) { 40 filas_p = filas_a; 41 columnas_p = columnas_b; 42 for (i=0; i<filas_p; i++) 43 for (j=0; j<columnas_p; j++) { 44 p[i][j] = 0.0; 45 for (k=0; k<columnas_a; k++) 46 p[i][j] += a[i][k] * b[k][j]; 47 } 48 } 49 50 /* Impresi´on del resultado de la suma */ 51 if (filas_a == filas_b && columnas_a == columnas_b) { 52 printf ("Suman"); 53 for (i=0; i<filas_s; i++) { 54 for (j=0; j<columnas_s; j++) 55 printf ("%8.3f", s[i][j]); 56 printf ("n"); 57 } 58 } 59 else 60 printf ("Matrices no compatibles para la suma.n"); 61 62 /* Impresi´on del resultado del producto */ 63 if (columnas_a == filas_b) { 64 printf ("Producton"); Introducci´on a la Programaci´on con C 115
  • 122. 2.3 Vectores multidimensionales 65 for (i=0; i<filas_p; i++) { 66 for (j=0; j<columnas_p; j++) 67 printf ("%8.3f", p[i][j]); 68 printf ("n"); 69 } 70 } 71 else 72 printf ("Matrices no compatibles para el producto.n"); 73 74 return 0; 75 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 129 Extiende el programa de calculadora matricial para efectuar las siguientes operaciones: Producto de una matriz por un escalar. (La matriz resultante tiene la misma dimensi´on que la original y cada elemento se obtiene multiplicando el escalar por la celda correspondiente de la matriz original.) Transpuesta de una matriz. (La transpuesta de una matriz de n × m es una matriz de m × n en la que el elemento de la fila i y columna j tiene el mismo valor que el que ocupa la celda de la fila j y columna i en la matriz original.) · 130 Una matriz tiene un valle si el valor de una de sus celdas es menor que el de cualquiera de sus 8 celdas vecinas. Dise˜na un programa que lea una matriz (el usuario te indicar´a de cu´antas filas y columnas) y nos diga si la matriz tiene un valle o no. En caso afirmativo, nos mostrar´a en pantalla las coordenadas de todos los valles, sus valores y el de sus celdas vecinas. La matriz debe tener un n´umero de filas y columnas mayor o igual que 3 y menor o igual que 10. Las casillas que no tienen 8 vecinos no se consideran candidatas a ser valle (pues no tienen 8 vecinos). Aqu´ı tienes un ejemplo de la salida esperada para esta matriz de 4 × 5:     1 2 9 5 5 3 2 9 4 5 6 1 8 7 6 6 3 8 0 9     Valle en fila 2 columna 4: 9 5 5 9 4 5 8 7 6 Valle en fila 3 columna 2: 3 2 9 6 1 8 6 3 8 (Observa que al usuario se le muestran filas y columnas numeradas desde 1, y no desde 0.) · 131 Modifica el programa del ejercicio anterior para que considere candidata a valle a cualquier celda de la matriz. Si una celda tiene menos de 8 vecinos, se considera que la celda es valle si su valor es menor que el de todos ellos. Para la misma matriz del ejemplo del ejercicio anterior se obtendr´ıa esta salida: Valle en fila 1 columna 1: x x x x 1 2 x 3 2 Valle en fila 2 columna 4: 9 5 5 9 4 5 8 7 6 Valle en fila 3 columna 2: 3 2 9 6 1 8 116 Introducci´on a la Programaci´on con C
  • 123. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros 6 3 8 Valle en fila 4 columna 4: 8 7 6 8 0 9 x x x . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.3. Vectores de cadenas, matrices de caracteres Por lo dicho hasta el momento, est´a claro que un vector de cadenas es una matriz de caracteres. Este fragmento de programa, por ejemplo, declara un vector de 10 cadenas cuya longitud es menor o igual que 80: #define MAXLON 80 char v[10][MAXLON+1]; Cada fila de la matriz es una cadena y, como tal, debe terminar en un car´acter nulo. Este fragmento declara e inicializa un vector de tres cadenas: #define MAXLON 80 char v[3][MAXLON+1] = {"una", "dos", "tres" }; Puedes leer individualmente cada cadena por teclado: matriz cadenas.c matriz cadenas.c 1 #include <stdio.h> 2 3 #define MAXLON 80 4 5 int main(void) 6 { 7 char v[3][MAXLON+1]; 8 int i; 9 10 for (i=0; i<3; i++) { 11 printf ("Introduzca cadena: "); 12 gets(v[i]); 13 printf ("Cadena le´ıda: %sn", v[i]); 14 } 15 16 return 0; 17 } Vamos a desarrollar un programa ´util que hace uso de un vector de caracteres: un peque˜no corrector ortogr´afico para ingl´es. El programa dispondr´a de una lista de palabras en ingl´es (que encontrar´as en la p´agina web de la asignatura, en el fichero ingles.h), solicitar´a al usuario que introduzca por teclado un texto en ingl´es y le informar´a de qu´e palabras considera err´oneas por no estar inclu´ıdas en su diccionario. Aqu´ı tienes un ejemplo de uso del programa: Introduce una frase: does this sentence contiene only correct words, eh? palabra no encontrada: contiene palabra no encontrada: eh El fichero ingles.h es una cabecera de la que te mostramos ahora las primeras y ´ultimas l´ıneas: ingles.h ingles.h 1 #define DICCPALS 45378 2 #define MAXLONPAL 28 3 char diccionario[DICCPALS][MAXLONPAL+1] = { Introducci´on a la Programaci´on con C 117
  • 124. 2.3 Vectores multidimensionales 4 "aarhus", 5 "aaron", 6 "ababa", 7 "aback", 8 "abaft", 9 "abandon", 10 "abandoned", 11 "abandoning", 12 "abandonment", . . . 45376 "zorn", 45377 "zoroaster", 45378 "zoroastrian", 45379 "zulu", 45380 "zulus", 45381 "zurich" 45382 }; La variable diccionario es un vector de cadenas (o una matriz de caracteres, seg´un lo veas) donde cada elemento es una palabra inglesa en min´usculas. La constante DICCPALS nos indica el n´umero de palabras que contiene el diccionario y MAXLONPAL es la longitud de la palabra m´as larga (28 bytes), por lo que reservamos espacio para MAXLONPAL+1 caracteres (29 bytes: 28 m´as el correspondiente al terminador nulo). Las primeras l´ıneas de nuestro programa son ´estas: corrector.c 1 #include <stdio.h> 2 #include "ingles.h" F´ıjate en que inclu´ımos el fichero ingles.h encerrando su nombre entre comillas dobles, y no entre < y >. Hemos de hacerlo as´ı porque ingles.h es una cabecera nuestra y no reside en los directorios est´andar del sistema (m´as sobre esto en el siguiente cap´ıtulo). El programa empieza solicitando una cadena con gets. A continuaci´on, la dividir´a en un nuevo vector de palabras. Supondremos que una frase no contiene m´as de 100 palabras y que una palabra es una secuencia cualquiera de letras. Si el usuario introduce m´as de 100 palabras, le advertiremos de que el programa s´olo corrige las 100 primeras. Una vez formada la lista de palabras de la frase, el programa buscar´a cada una de ellas en el diccionario. Las que no est´en, se mostrar´an en pantalla precedidas del mensaje: palabra no encontrada. Vamos all´a: empezaremos por la lectura de la frase y su descomposici´on en una lista de palabras. corrector 1.c corrector.c 1 #include <stdio.h> 2 #include "ingles.h" 3 #include <string.h> 4 #include <ctype.h> 5 6 #define MAXLONFRASE 1000 7 #define MAXPALSFRASE 100 8 #define MAXLONPALFRASE 100 9 10 int main(void) 11 { 12 char frase[MAXLONFRASE+1]; 13 char palabra[MAXPALSFRASE][MAXLONPALFRASE+1]; 14 int palabras; // N´umero de palabras en la frase 15 int lonfrase, i, j; 16 17 /* Lectura de la frase */ 18 printf ("Introduce una frase: "); 19 gets(frase); 20 21 lonfrase = strlen(frase); 22 118 Introducci´on a la Programaci´on con C
  • 125. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros 23 /* Descomposici´on en un vector de palabras */ 24 i = 0; 25 while (i<lonfrase && !isalpha(frase[i])) i++; // Saltarse las no-letras iniciales. 26 27 palabras = 0; 28 while (i<lonfrase) { // Recorrer todos los caracteres 29 30 // Avanzar mientras vemos caracteres e ir formando la palabra palabra[palabras]. 31 j = 0; 32 while (i<lonfrase && isalpha(frase[i])) palabra[palabras][j++] = frase[i++]; 33 palabra[palabras][j] = ’0’; // El terminador es responsabilidad nuestra. 34 35 // Incrementar el contador de palabras. 36 palabras++; 37 if (palabras == MAXPALSFRASE) // Y finalizar si ya no caben m´as palabras 38 break; 39 40 // Saltarse las no-letras que separan esta palabra de la siguiente (si las hay). 41 while (i<lonfrase && !isalpha(frase[i])) i++; 42 } 43 44 /* Comprobaci´on de posibles errores */ 45 for (i=0; i<palabras; i++) 46 printf ("%sn", palabra[i]); 47 48 return 0; 49 } ¡Buf! Complicado, ¿no? ¡Ya estamos echando en falta el m´etodo split de Python! No nos viene mal probar si nuestro c´odigo funciona mostrando las palabras que ha encontrado en la frase. Por eso hemos a˜nadido las l´ıneas 44–46. Una vez hayas ejecutado el programa y comprobado que funciona correctamente hasta este punto, comenta el bucle que muestra las palabras: 44 /* Comprobaci´on de posibles errores */ 45 // for (i=0; i<palabras; i++) 46 // printf ("%sn", palabra[i]); . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 132 Un programador, al copiar el programa, ha sustituido la l´ınea que reza as´ı: while (i<lonfrase && !isalpha(frase[i])) i++; // Saltarse las no-letras iniciales. por esta otra: while (frase[i] != ’0’ && !isalpha(frase[i])) i++; // Saltarse las no-letras iniciales. ¿Es correcto el programa resultante? ¿Por qu´e? · 133 Un programador, al copiar el programa, ha sustituido la l´ınea que reza as´ı: while (i<lonfrase && !isalpha(frase[i])) i++; // Saltarse las no-letras iniciales. por esta otra: while (frase[i] && !isalpha(frase[i])) i++; // Saltarse las no-letras iniciales. ¿Es correcto el programa resultante? ¿Por qu´e? · 134 Un programador, al copiar el programa, ha sustituido la l´ınea que reza as´ı: while (i<lonfrase && isalpha(frase[i])) palabra[palabras][j++] = frase[i++]; por esta otra: while (isalpha(frase[i])) palabra[palabras][j++] = frase[i++]; ¿Es correcto el programa resultante? ¿Por qu´e? Introducci´on a la Programaci´on con C 119
  • 126. 2.3 Vectores multidimensionales · 135 Un programador, al copiar el programa, ha sustituido la l´ınea que reza as´ı: while (i<lonfrase && !isalpha(frase[i])) i++; // Saltarse las no-letras iniciales. por esta otra: while (!isalpha(frase[i])) palabra[palabras][j++] = frase[i++]; ¿Es correcto el programa resultante? ¿Por qu´e? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sigamos. Nos queda la b´usqueda de cada palabra en el diccionario. Una primera idea consiste en buscar cada palabra de la frase recorriendo el diccionario desde la primera hasta la ´ultima entrada: corrector 2.c corrector.c . . . 48 49 /* ? Est´an todas las palabras en el diccionario? */ 50 for (i=0; i<palabras; i++) { 51 encontrada = 0; 52 for (j=0; j<DICCPALS; j++) 53 if (strcmp(palabra[i],diccionario[j]) == 0) { // ? Es palabra[i] igual que diccionario[j]? 54 encontrada = 1; 55 break; 56 } 57 if (!encontrada) 58 printf ("palabra no encontrada: %sn", palabra[i]); 59 } 60 return 0; 61 } Ten en cuenta lo que hace strcmp: recorre las dos cadenas hasta encontrar alguna diferencia entre ellas o concluir que son id´enticas. Es, por tanto, una operaci´on bastante costosa en tiempo. ¿Podemos reducir el n´umero de comparaciones? ¡Claro! Como el diccionario est´a ordenado al- fab´eticamente, podemos abortar el recorrido cuando llegamos a una voz del diccionario posterior (seg´un el orden alfab´etico) a la que buscamos: corrector 3.c corrector.c . . . 48 49 /* ? Est´an todas las palabras en el diccionario? */ 50 for (i=0; i<palabras; i++) { 51 encontrada = 0; 52 for (j=0; j<DICCPALS; j++) 53 if (strcmp(palabra[i],diccionario[j]) == 0) { // ? Es palabra[i] igual que diccionario[j]? 54 encontrada = 1; 55 break; 56 } 57 else if (strcmp(palabra[i], diccionario[j]) < 0) // ? palabra[i] < diccionario[j]? 58 break; 59 if (!encontrada) 60 printf ("palabra no encontrada: %sn", palabra[i]); 61 } 62 return 0; 63 } Con esta mejora hemos intentado reducir a la mitad el n´umero de comparaciones con cadenas del diccionario, pero no hemos logrado nuestro objetivo: ¡aunque, en promedio, efectuamos comparaciones con la mitad de las palabras del diccionario, estamos llamando dos veces a strcmp! Es mejor almacenar el resultado de una sola llamada a strcmp en una variable: corrector 4.c corrector.c . . . 48 120 Introducci´on a la Programaci´on con C
  • 127. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros 49 /* ? Est´an todas las palabras en el diccionario? */ 50 for (i=0; i<palabras; i++) { 51 encontrada = 0; 52 for (j=0; j<DICCPALS; j++) { 53 comparacion = strcmp(palabra[i], diccionario[j]); 54 if (comparacion == 0) { // ? Es palabra[i] igual que diccionario[j]? 55 encontrada = 1; break; 56 } 57 else if (comparacion < 0) // ? Es palabra[i] menor que diccionario[j]? 58 break; 59 } 60 if (!encontrada) 61 printf ("palabra no encontrada: %sn", palabra[i]); 62 } 63 return 0; 64 } (Recuerda declarar comparacion como variable de tipo entero.) El diccionario tiene 45378 palabras. En promedio efectuamos, pues, 22689 comparaciones por cada palabra de la frase. Mmmm. A´un podemos hacerlo mejor. Si la lista est´a ordenada, podemos efectuar una b´usqueda dicot´omica. La b´usqueda dicot´omica efect´ua un n´umero de comparaciones reducid´ısimo: ¡bastan 16 comparaciones para decidir si una palabra cualquiera est´a o no en el diccionario! corrector.c . . . 97 98 /* ? Est´an todas las palabras en el diccionario? */ 99 for (i=0; i<palabras; i++) { 100 encontrada = 0; 101 izquierda = 0; 102 derecha = DICCPALS; 103 104 while (izquierda < derecha) { 105 j = (izquierda + derecha) / 2; 106 comparacion = strcmp(palabra[i], diccionario[j]); 107 if (comparacion < 0) 108 derecha = j; 109 else if (comparacion > 0) 110 izquierda = j+1; 111 else { 112 encontrada = 1; 113 break; 114 } 115 } 116 117 if (!encontrada) 118 printf ("palabra no encontrada: %sn", palabra[i]); 119 } 120 121 return 0; 122 } (Debes declarar derecha e izquierda como enteros.) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 136 Escribe un programa C que lea un texto (de longitud menor que 1000) y obtenga un vector de cadenas en el que cada elemento es una palabra distinta del texto (con un m´aximo de 500 palabras). Muestra el contenido del vector por pantalla. · 137 Modifica el programa del ejercicio anterior para que el vector de palabras se muestre en pantalla ordenado alfab´eticamente. Deber´as utilizar el m´etodo de la burbuja para ordenar el vector. Introducci´on a la Programaci´on con C 121
  • 128. 2.3 Vectores multidimensionales · 138 Representamos la baraja de cartas con un vector de cadenas. Los palos son "oros ", "copas", "espadas" y "bastos". Las cartas con n´umeros entre 2 y 9 se describen con el texto "n´umero de palo" (ejemplo: "2 de oros", "6 de copas"). Los ases se describen con la cadena "as de palo", las sotas con "sota de palo", los caballos con "caballo de palo" y los reyes con "rey de palo". Escribe un programa que genere la descripci´on de las 40 cartas de la baraja. Usa bucles siempre que puedas y comp´on las diferentes partes de cada descripci´on con strcat o sprintf . A continuaci´on, baraja las cartas utilizando para ello el generador de n´umeros aleatorios y muestra el resultado por pantalla. · 139 Dise˜na un programa de ayuda al diagn´ostico de enfermedades. En nuestra base de datos hemos registrado 10 enfermedades y 10 s´ıntomas: 1 char enfermedades[10][20] = { "gripe", "indigesti´on", "catarro", ... }; 2 char sintomas[10][20] = { "fiebre", "tos", "dolor de cabeza", ... }; Almacenamos en una matriz de 10 × 10 valores booleanos (1 o 0) los s´ıntomas que presenta cada enfermedad: 1 char sintomatologia[10][10] = {{ 1, 0, 1, ... }, 2 { 0, 0, 0, ... }, 3 ... 4 }; La celda sintomatologia[i][j] vale 1 si la enfermedad i presenta el s´ıntoma j, y 0 en caso contrario. Dise˜na un programa que pregunte al paciente si sufre cada uno de los 10 s´ıntomas y, en funci´on de las respuestas dadas, determine la enfermedad que padece. Si la descripci´on de sus s´ıntomas no coincide exactamente con la de alguna de las enfermedades, el sistema indicar´a que no se puede emitir un diagn´ostico fiable. · 140 Modifica el programa anterior para que, cuando no hay coincidencia absoluta de s´ıntomas, muestre las tres enfermedades con sintomatolog´ıa m´as parecida. Si, por ejemplo, una enfermedad presenta 9 coincidencias con la sintomatolog´ıa del paciente, el sistema mostrar´a el nombre de la enfermedad y el porcentaje de confianza del diagn´ostico (90%). · 141 Vamos a implementar un programa que nos ayude a traducir texto a c´odigo Morse. Aqu´ı tienes una tabla con el c´odigo Morse: A B C D E F G H I J K L .- -... -.-. -.. . ..-. --. .... .. .--- -.- .-.. M N O P Q R S T U V W X -- -. --- .--. --.- .-. ... - ..- ...- .-- -..- Y Z 0 1 2 3 4 5 6 7 8 9 -.-- --.. ----- .---- ..--- ...-- ....- ..... -.... --... ---.. ----. El programa leer´a una l´ınea y mostrar´a por pantalla su traducci´on a c´odigo Morse. Ten en cuenta que las letras se deben separar por pausas (un espacio blanco) y las palabras por pausas largas (tres espacios blancos). Los acentos no se tendr´an en cuenta al efectuar la traducci´on (la letra ´A, por ejemplo, se representar´a con .-) y la letra ’~N’ se mostrar´a como una ’N’. Los signos que no aparecen en la tabla (comas, admiraciones, etc.) no se traducir´an, excepci´on hecha del punto, que se traduce por la palabra STOP. Te conviene pasar la cadena a may´usculas (o efectuar esta transformaci´on sobre la marcha), pues la tabla Morse s´olo recoge las letras may´usculas y los d´ıgitos. Por ejemplo, la cadena "Hola, mundo." se traducir´a por .... --- .-.. .- -- ..- -. -.. --- ... - --- .--. Debes usar un vector de cadenas para representar la tabla de traducci´on a Morse. El c´odigo Morse de la letra ’A’, por ejemplo, estar´a accesible como una cadena en morse[’A’]. (Tal vez te sorprenda la notaci´on morse[’A’]. Recuerda que ’A’ es el n´umero 65, pues el car´acter ’A’ tiene ese valor ASCII. As´ı pues, morse[’A’] y morse[65] son lo mismo. Por cierto: el vector de cadenas morse s´olo tendr´a c´odigos para las letras may´usculas y los d´ıgitos; recuerda inicializar el resto de componentes con la cadena vac´ıa.) 122 Introducci´on a la Programaci´on con C
  • 129. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros · 142 Escribe un programa que lea un texto escrito en c´odigo Morse y lo traduzca al c´odigo alfab´etico. Si, por ejemplo, el programa lee por teclado esta cadena: ".... --- .-.. .- -- ..- -. -.. --- ... - --- .--." mostrar´a en pantalla el texto HOLAMUNDOSTOP. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4. Registros Los vectores permiten agrupar varios elementos de un mismo tipo. Cada elemento de un vector es accesible a trav´es de un ´ındice. En ocasiones necesitar´as agrupar datos de diferentes tipos y/o preferir´as acceder a diferentes elementos de un grupo de datos a trav´es de un identificador, no de un ´ındice. Los registros son agrupaciones heterog´eneas de datos cuyos elementos (denominados campos) son accesibles mediante identificadores. Ya hemos estudiado registros en Python, as´ı que el concepto y su utilidad han de resultarte familiares. Veamos ahora un dise˜no t´ıpico de registro. Supongamos que deseamos mantener los siguien- tes datos de una persona: su nombre (con un m´aximo de 40 caracteres), su edad (un entero), su DNI (una cadena de 9 caracteres). Podemos definir un registro ((persona)) antes de la aparici´on de main: #define MAXNOM 40 #define LONDNI 9 struct Persona { char nombre[MAXNOM+1]; int edad; char dni[LONDNI+1]; }; // <- F´ıjate en el punto y coma: es f´acil olvidarse de ponerlo. La definici´on de un registro introduce un nuevo tipo de datos en nuestro programa. En el ejemplo hemos definido el tipo struct Persona (la palabra struct forma parte del nombre del tipo). Ahora puedes declarar variables de tipo struct Persona as´ı: struct Persona pepe, juan, ana; En tu programa puedes acceder a cada uno de los campos de una variable de tipo struct sepa- rando con un punto el identificador de la variable del correspondiente identificador del campo. Por ejemplo, pepe.edad es la edad de Pepe (un entero que ocupa cuatro bytes), juan.nombre es el nombre de Juan (una cadena), y ana.dni[8] es la letra del DNI de Ana (un car´acter). Cada variable de tipo struct Persona ocupa, en principio, 55 bytes: 41 por el nombre, 4 por la edad y 10 por el DNI. (Si quieres saber por qu´e hemos resaltado lo de ((en principio)), lee el cuadro ((Alineamientos)).) Este programa ilustra c´omo acceder a los campos de un registro leyendo por teclado sus valores y mostrando por pantalla diferentes informaciones almacenadas en ´el: registro.c 1 #include <stdio.h> 2 #include <string.h> 3 4 #define MAXNOM 40 5 #define LONDNI 9 6 7 struct Persona { 8 char nombre[MAXNOM+1]; 9 int edad; Introducci´on a la Programaci´on con C 123
  • 130. 2.4 Registros Alineamientos El operador sizeof devuelve el tama˜no en bytes de un tipo o variable. Analiza este programa: alineamiento.c alineamiento.c 1 #include <stdio.h> 2 3 struct Registro { 4 char a; 5 int b; 6 }; 7 8 int main(void) 9 { 10 printf ("Ocupaci´on: %d bytesn", sizeof(struct Registro)); 11 return 0; 12 } Parece que vaya a mostrar en pantalla el mensaje ((Ocupaci´on: 5 bytes)), pues un char ocupa 1 byte y un int ocupa 4. Pero no es as´ı: Ocupaci´on: 8 bytes La raz´on de que ocupe m´as de lo previsto es la eficiencia. Los ordenadores con arqui- tectura de 32 bits agrupan la informaci´on en bloques de 4 bytes. Cada uno de esos bloques se denomina ((palabra)). Cada acceso a memoria permite traer al procesador los 4 bytes de una palabra. Si un dato est´a a caballo entre dos palabras, requiere dos accesos a memoria, afectando seriamente a la eficiencia del programa. El compilador trata de generar un pro- grama eficiente y da prioridad a la velocidad de ejecuci´on frente al consumo de memoria. En nuestro caso, esta prioridad se ha traducido en que el segundo campo se almacene en una palabra completa, aunque ello suponga desperdiciar 3 bytes en el primero de los campos. 10 char dni[LONDNI+1]; 11 }; 12 13 int main(void) 14 { 15 struct Persona ejemplo; 16 char linea[81]; 17 int i, longitud; 18 19 printf ("Nombre: "); gets(ejemplo.nombre); 20 printf ("Edad : "); gets(linea); sscanf (linea, "%d", &ejemplo.edad); 21 printf ("DNI : "); gets(ejemplo.dni); 22 23 printf ("Nombre le´ıdo: %sn", ejemplo.nombre); 24 printf ("Edad le´ıda : %dn", ejemplo.edad); 25 printf ("DNI le´ıdo : %sn", ejemplo.dni); 26 27 printf ("Iniciales del nombre: "); 28 longitud = strlen(ejemplo.nombre); 29 for (i=0; i<longitud; i++) 30 if (ejemplo.nombre[i] >= ’A’ && ejemplo.nombre[i] <= ’Z’) 31 printf ("%c", ejemplo.nombre[i]); 32 printf ("n"); 33 34 printf ("Letra del DNI: "); 35 longitud = strlen(ejemplo.dni); 36 if (ejemplo.dni[longitud-1] < ’A’ || ejemplo.dni[longitud-1] > ’Z’) 37 printf ("No tiene letra.n"); 38 else 39 printf ("%cn", ejemplo.dni[longitud-1]); 124 Introducci´on a la Programaci´on con C
  • 131. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros 40 41 return 0; 42 } Los registros pueden copiarse´ıntegramente sin mayor problema. Este programa, por ejemplo, copia el contenido de un registro en otro y pasa a min´usculas el nombre de la copia: copia registro.c copia registro.c 1 #include <stdio.h> 2 #include <string.h> 3 #include <ctype.h> 4 5 #define MAXNOM 40 6 #define LONDNI 9 7 8 struct Persona { 9 char nombre[MAXNOM+1]; 10 int edad; 11 char dni[LONDNI+1]; 12 }; 13 14 int main(void) 15 { 16 struct Persona una, copia; 17 char linea[81]; 18 int i, longitud; 19 20 printf ("Nombre: "); gets(una.nombre); 21 printf ("Edad : "); gets(linea); sscanf (linea, "%d", &una.edad); 22 printf ("DNI : "); gets(una.dni); 23 24 copia = una; // Copia 25 26 longitud = strlen(copia.nombre); 27 for (i=0; i<longitud; i++) 28 copia.nombre[i] = tolower(copia.nombre[i]); 29 30 printf ("Nombre le´ıdo: %sn", una.nombre); 31 printf ("Edad le´ıda : %dn", una.edad); 32 printf ("DNI le´ıdo : %sn", una.dni); 33 34 printf ("Nombre copia: %sn", copia.nombre); 35 printf ("Edad copia : %dn", copia.edad); 36 printf ("DNI copia : %sn", copia.dni); 37 38 return 0; 39 } Observa que la copia se efect´ua incluso cuando los elementos del registro son vectores. O sea, copiar vectores con una mera asignaci´on est´a prohibido, pero copiar registros es posible. Un poco incoherente, ¿no? Por otra parte, no puedes comparar registros. Este programa, por ejemplo, efect´ua una copia de un registro en otro para, a continuaci´on, intentar decirnos si ambos son iguales o no: E compara registros mal.c E 1 #include <stdio.h> 2 3 #define MAXNOM 40 4 #define LONDNI 9 5 6 struct Persona { 7 char nombre[MAXNOM+1]; 8 int edad; 9 char dni[LONDNI+1]; Introducci´on a la Programaci´on con C 125
  • 132. 2.4 Registros 10 }; 11 12 int main(void) 13 { 14 struct Persona una, copia; 15 char linea[81]; 16 int i, longitud; 17 18 printf ("Nombre: "); gets(una.nombre); 19 printf ("Edad : "); gets(linea); sscanf (linea, "%d", &una.edad); 20 printf ("DNI : "); gets(una.dni); 21 22 copia = una; // Copia 23 24 if (copia == una ) // Comparaci´on ilegal. 25 printf ("Son igualesn"); 26 else 27 printf ("No son igualesn"); 28 29 return 0; 30 } Pero ni siquiera es posible compilarlo. La l´ınea 24 contiene un error que el compilador se˜nala co- mo ((invalid operands to binary ==)), o sea, ((operandos inv´alidos para la operaci´on binaria ==)). Entonces, ¿c´omo podemos decidir si dos registros son iguales? Comprobando la igualdad de cada uno de los campos de un registro con el correspondiente campo del otro: compara registros.c compara registros.c 1 #include <stdio.h> 2 3 #define MAXNOM 40 4 #define LONDNI 9 5 6 struct Persona { 7 char nombre[MAXNOM+1]; 8 int edad; 9 char dni[LONDNI+1]; 10 }; 11 12 int main(void) 13 { 14 struct Persona una, copia; 15 char linea[81]; 16 int i, longitud; 17 18 printf ("Nombre: "); gets(una.nombre); 19 printf ("Edad : "); gets(linea); scanf (linea, "%d", &una.edad); 20 printf ("DNI : "); gets(una.dni); 21 22 copia = una; // Copia 23 24 if (strcmp(copia.nombre, una.nombre)==0 && copia.edad==una.edad 25 && strcmp(copia.dni, una.dni)==0) 26 printf ("Son igualesn"); 27 else 28 printf ("No son igualesn"); 29 30 return 0; 31 } 126 Introducci´on a la Programaci´on con C
  • 133. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros Una raz´on para no comparar Si C sabe copiar una estructura ((bit a bit)), ¿por qu´e no sabe compararlas ((bit a bit))? El problema estriba en construcciones como las cadenas que son campos de un registro. Considera esta definici´on: struct Persona { char nombre[10]; char apellido[10]; }; Cada dato de tipo struct Persona ocupa 20 bytes. Si una persona a tiene su campo a.nombre con valor "Pepe", s´olo los cinco primeros bytes de su nombre tienen un valor bien definido. Los cinco siguientes pueden tener cualquier valor aleatorio. Otro registro b cuyo campo b.nombre tambi´en valga "Pepe" (y tenga id´entico apellido) puede tener valores diferentes en su segundo grupo de cinco bytes. Una comparaci´on ((bit a bit)) nos dir´ıa que los registros son diferentes. La asignaci´on no entra˜na este tipo de problema, pues la copia es ((bit a bit)). Como mucho, resulta algo ineficiente, pues copiar´a hasta los bytes de valor indefinido. Una forma de inicializaci´on C permite inicializar registros de diferentes modos, algunos bastante interesantes desde el punto de vista de la legibilidad. Este programa, por ejemplo, define un struct y crea e inicializa de diferentes formas, pero con el mismo valor, varias variables de este tipo: struct Algo { int x; char nombre[10]; float y; }; ... struct Algo a = { 1, "Pepe", 2.0 }; struct Algo b = { .x = 1, .nombre = "Pepe", .y = 2.0 }; struct Algo c = { .nombre = "Pepe", .y = 2.0, .x = 1}; struct Algo d; ... d.x = 1; strcpy(d.nombre, "Pepe"); d.y = 2.0; 2.4.1. Un ejemplo: registros para almacenar vectores de talla variable (pero acotada) Los vectores est´aticos tienen una talla fija. Cuando necesitamos un vector cuya talla var´ıa o no se conoce hasta iniciada la ejecuci´on del programa usamos un truco: definimos un vector cuya talla sea suficientemente grande para la tarea que vamos a abordar y mantenemos la ((talla real)) en una variable. Lo hemos hecho con el programa que calcula algunas estad´ısticas con una serie de edades: defin´ıamos un vector edad con capacidad para almacenar la edad de MAX_PERSONAS y una variable personas, cuyo valor siempre era menor o igual que MAX_PERSONAS, nos indicaba cu´antos elementos del vector conten´ıan realmente datos. Hay algo poco elegante en esa soluci´on: las variables edad y personas son variables independientes, que no est´an relacionadas entre s´ı en el programa (salvo por el hecho de que nosotros sabemos que s´ı lo est´an). Una soluci´on m´as elegante pasa por crear un registro que contenga el n´umero de personas y, en un vector, las edades. He aqu´ı el programa que ya te presentamos en su momento convenientemente modificado seg´un este nuevo principio de dise˜no: Introducci´on a la Programaci´on con C 127
  • 134. 2.4 Registros edades 8.c edades.c 1 #include <stdio.h> 2 #include <math.h> 3 4 #define MAX_PERSONAS 20 5 6 struct ListaEdades { 7 int edad[MAX_PERSONAS]; // Vector con capacidad para MAX PERSONAS edades. 8 int talla; // N´umero de edades realmente almacenadas. 9 }; 10 11 int main(void) 12 { 13 struct ListaEdades personas; 14 int i, j, aux, suma_edad; 15 float suma_desviacion, media, desviacion; 16 int moda, frecuencia, frecuencia_moda, mediana; 17 18 /* Lectura de edades */ 19 personas.talla = 0; 20 do { 21 printf ("Introduce edad de la persona %d (si es negativa, acabar): ", 22 personas.talla +1); 23 scanf ("%d", &personas.edad[personas.talla ]); 24 personas.talla ++; 25 } while (personas.talla < MAX_PERSONAS && personas.edad [personas.talla -1] >= 0); 26 personas.talla --; 27 28 if (personas.talla > 0) { 29 /* C´alculo de la media */ 30 suma_edad = 0; 31 for (i=0; i<personas.talla ; i++) 32 suma_edad += personas.edad[i]; 33 media = suma_edad / (float)personas.talla ; 34 35 /* C´alculo de la desviacion t´ıpica */ 36 suma_desviacion = 0.0; 37 for (i=0; i<personas.talla ; i++) 38 suma_desviacion += (personas.edad[i] - media) * (personas.edad[i] - media); 39 desviacion = sqrt( suma_desviacion / personas.talla ); 40 41 /* C´alculo de la moda */ 42 for (i=0; i<personas.talla -1; i++) // Ordenaci´on mediante burbuja. 43 for (j=0; j<personas.talla -i; j++) 44 if (personas.edad[j] > personas.edad[j+1]) { 45 aux = personas.edad[j]; 46 personas.edad[j] = personas.edad[j+1]; 47 personas.edad[j+1] = aux; 48 } 49 50 frecuencia = 0; 51 frecuencia_moda = 0; 52 moda = -1; 53 for (i=0; i<personas.talla -1; i++) 54 if (personas.edad[i] == personas.edad[i+1]) 55 if (++frecuencia > frecuencia_moda) { 56 frecuencia_moda = frecuencia; 57 moda = personas.edad[i]; 58 } 59 else 60 frecuencia = 0; 61 62 /* C´alculo de la mediana */ 128 Introducci´on a la Programaci´on con C
  • 135. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros 63 mediana = personas.edad[personas.talla/2]; 64 65 /* Impresi´on de resultados */ 66 printf ("Edad media : %fn", media); 67 printf ("Desv. t´ıpica: %fn", desviacion); 68 printf ("Moda : %dn", moda); 69 printf ("Mediana : %dn", mediana); 70 } 71 else 72 printf ("No se introdujo dato alguno.n"); 73 74 return 0; 75 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 143 Modifica el programa de c´alculo con polinomios que sirvi´o de ejemplo en el apar- tado 2.1.5 para representar los polinomios mediante registros. Cada registro contendr´a dos campos: el grado del polinomio y el vector con los coeficientes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.2. Un ejemplo: rectas de regresi´on para una serie de puntos en el plano Hay m´etodos estad´ısticos que permiten obtener una recta que se ajusta de forma ´optima a una serie de puntos en el plano. y = mx + b Si disponemos de una serie de n puntos (x1, y1), (x2, y2), . . . , (xn, yn), la recta de ajuste y = mx+b que minimiza el cuadrado de la distancia vertical de todos los puntos a la recta se puede obtener efectuando los siguientes c´alculos: m = ( n i=1 xi) · ( n i=1 yi) − n · n i=1 xiyi ( n i=1 xi) 2 − n · n i=1 x2 i , b = ( n i=1 yi) · n i=1 x2 i − ( n i=1 xi) · ( n i=1 xiyi) n n i=1 x2 i − ( n i=1 xi) 2 . Las f´ormulas asustan un poco, pero no contienen m´as que sumatorios. El programa que vamos a escribir lee una serie de puntos (con un n´umero m´aximo de, pongamos, 1000), y muestra los valores de m y b. Modelaremos los puntos con un registro: struct Punto { float x, y; }; El vector de puntos, al que en principio denominaremos p, tendr´a talla 1000: #define TALLAMAX 1000 struct Punto p[TALLAMAX]; Pero 1000 es el n´umero m´aximo de puntos. El n´umero de puntos disponibles efectivamente ser´a menor o igual y su valor deber´a estar accesible en alguna variable. Olvid´emonos del vector p: nos conviene definir un registro en el que se almacenen vector y talla real del vector. Introducci´on a la Programaci´on con C 129
  • 136. 2.4 Registros struct ListaPuntos { struct Punto punto[TALLAMAX]; int talla; }; Observa que estamos anidando structs. Necesitamos ahora una variable del tipo que hemos definido: 1 #include <stdio.h> 2 3 #define TALLAMAX 1000 4 5 struct Punto { 6 float x, y; 7 }; 8 9 struct ListaPuntos { 10 struct Punto punto[TALLAMAX]; 11 int talla; 12 }; 13 14 int main(void) 15 { 16 struct ListaPuntos lista ; 17 ... Reflexionemos brevemente sobre c´omo podemos acceder a la informaci´on de la variable lista: Expresi´on Tipo y significado lista Es un valor de tipo struct ListaPuntos. Contiene un vector de 1000 puntos y un entero. lista.talla Es un entero. Indica cu´antos elementos del vector contienen informaci´on. lista.punto Es un vector de 1000 valores de tipo struct Punto. lista.punto[0] Es el primer elemento del vector y es de tipo struct Punto, as´ı que est´a compuesto por dos flotantes. lista.punto[0].x Es el campo x del primer elemento del vector. Su tipo es float. lista.punto[lista.talla-1].y Es el campo y del ´ultimo elemento con informaci´on del vec- tor. Su tipo es float. lista.punto.x ¡Error! Si lista.punto es un vector, no podemos acceder al campo x. lista.punto.x[0] ¡Error! Si lo anterior era incorrecto, ´esto lo es a´un m´as. lista.punto.[0].x ¡Error! ¿Qu´e hace un punto antes del operador de indexa- ci´on? lista[0].punto ¡Error! La variable lista no es un vector, as´ı que no puedes aplicar el operador de indexaci´on sobre ella. Ahora que tenemos m´as claro c´omo hemos modelado la informaci´on, vamos a resolver el problema propuesto. Cada uno de los sumatorios se precalcular´a cuando se hayan le´ıdo los puntos. De ese modo, simplificaremos significativamente las expresiones de c´alculo de m y b. Debes tener en cuenta que, aunque en las f´ormulas se numeran los puntos empezando en 1, en C se empieza en 0. Veamos el programa completo: ajuste.c ajuste.c 1 #include <stdio.h> 2 3 #define TALLAMAX 1000 4 5 struct Punto { 6 float x, y; 130 Introducci´on a la Programaci´on con C
  • 137. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros 7 }; 8 9 struct ListaPuntos { 10 struct Punto punto[TALLAMAX]; 11 int talla; 12 }; 13 14 int main(void) 15 { 16 struct ListaPuntos lista; 17 18 float sx, sy, sxy, sxx; 19 float m, b; 20 int i; 21 22 /* Lectura de puntos */ 23 printf ("Puntos a leer: "); scanf ("%d", &lista.talla); 24 for (i=0; i<lista.talla; i++) { 25 printf ("Coordenada x del punto %d: ", i); scanf ("%f", &lista.punto[i].x); 26 printf ("Coordenada y del punto %d: ", i); scanf ("%f", &lista.punto[i].y); 27 } 28 29 /* C´alculo de los sumatorios */ 30 sx = 0.0; 31 for (i=0; i<lista.talla; i++) 32 sx += lista.punto[i].x; 33 34 sy = 0.0; 35 for (i=0; i<lista.talla; i++) 36 sy += lista.punto[i].y; 37 38 sxy = 0.0; 39 for (i=0; i<lista.talla; i++) 40 sxy += lista.punto[i].x * lista.punto[i].y; 41 42 sxx = 0.0; 43 for (i=0; i<lista.talla; i++) 44 sxx += lista.punto[i].x * lista.punto[i].x; 45 46 /* C´alculo de m y b e impresi´on de resultados */ 47 if (sx * sx - lista.talla * sxx == 0) 48 printf ("Indefinidan"); 49 else { 50 m = (sx * sy - lista.talla * sxy) / (sx * sx - lista.talla * sxx); 51 printf ("m = %fn", m); 52 b = (sy * sxx - sx * sxy) / (lista.talla * sxx - sx * sx); 53 printf ("b = %fn", b); 54 } 55 56 return 0; 57 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 144 Dise˜na un programa que lea una lista de hasta 1000 puntos por teclado y los almacene en una variable (del tipo que t´u mismo definas) llamada representantes. A continuaci´on, ir´a leyendo nuevos puntos hasta que se introduzca el punto de coordenadas (0, 0). Para cada nuevo punto, debes encontrar cu´al es el punto m´as pr´oximo de los almacenados en representantes. Calcula la distancia entre dos puntos como la distancia eucl´ıdea. · 145 Deseamos efectuar c´alculos con enteros positivos de hasta 1000 cifras, m´as de las que puede almacenar un int (o incluso long long int). Define un registro que permita representar n´umeros de hasta 1000 cifras con un vector en el que cada elemento es una cifra (representada con un char). Representa el n´umero de cifras que tiene realmente el valor almacenado con un Introducci´on a la Programaci´on con C 131
  • 138. 2.4 Registros campo del registro. Escribe un programa que use dos variables del nuevo tipo para leer dos n´umeros y que calcule el valor de la suma y la resta de estos (supondremos que la resta siempre proporciona un entero positivo como resultado). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.3. Otro ejemplo: gesti´on de una coleci´on de CDs Estamos en condiciones de abordar la implementaci´on de un programa moderadamente com- plejo: la gesti´on de una colecci´on de CDs (aunque, todav´ıa, sin poder leer/escribir en fichero). De cada CD almacenaremos los siguientes datos: el t´ıtulo (una cadena con, a lo sumo, 80 caracteres), el int´erprete (una cadena con, a lo sumo, 40 caracteres), la duraci´on (en minutos y segundos), el a˜no de publicaci´on. Definiremos un registro para almacenar los datos de un CD y otro para representar la duraci´on, ya que ´esta cuenta con dos valores (minutos y segundos): #define LONTITULO 80 #define LONINTERPRETE 40 struct Tiempo { int minutos; int segundos; }; struct CompactDisc { char titulo[LONTITULO+1]; char interprete[LONINTERPRETE+1]; struct Tiempo duracion; int anyo; }; Vamos a usar un vector para almacenar la colecci´on, definiremos un m´aximo n´umero de CDs: 1000. Eso no significa que la colecci´on tenga 1000 discos, sino que puede tener a lo sumo 1000. ¿Y cu´antos tiene en cada instante? Utilizaremos una variable para mantener el n´umero de CDs presente en la colecci´on. Mejor a´un: definiremos un nuevo tipo de registro que represente a la colecci´on entera de CDs. El nuevo tipo contendr´a dos campos: el vector de discos (con capacidad limitada a 1000 unidades), y el n´umero de discos en el vector. He aqu´ı la definici´on de la estructura y la declaraci´on de la colecci´on de CDs: #define MAXDISCOS 1000 ... struct Coleccion { struct CompactDisc cd[MAXDISCOS]; int cantidad; }; struct Coleccion mis_cds; Nuestro programa permitir´a efectuar las siguientes acciones: A˜nadir un CD a la base de datos. Listar toda la base de datos. 132 Introducci´on a la Programaci´on con C
  • 139. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros Listar los CDs de un int´erprete. Suprimir un CD dado su t´ıtulo y su int´erprete. (El programa no resultar´a muy ´util hasta que aprendamos a utilizar ficheros en C, pues al finalizar cada ejecuci´on se pierde toda la informaci´on registrada.) He aqu´ı el programa completo: discoteca.c discoteca.c 1 #include <stdio.h> 2 #include <string.h> 3 4 #define MAXLINEA 80 5 6 #define MAXDISCOS 1000 7 #define LONTITULO 80 8 #define LONINTERPRETE 40 9 10 enum { Anyadir=1, ListadoCompleto, ListadoPorInterprete, Suprimir, Salir }; 11 12 struct Tiempo { 13 int minutos; 14 int segundos; 15 }; 16 17 struct CompactDisc { 18 char titulo[LONTITULO+1]; 19 char interprete[LONINTERPRETE+1]; 20 struct Tiempo duracion; 21 int anyo; 22 }; 23 24 struct Coleccion { 25 struct CompactDisc cd[MAXDISCOS]; 26 int cantidad; 27 }; 28 29 int main(void) 30 { 31 struct Coleccion mis_cds; 32 int opcion, i, j; 33 char titulo[LONTITULO+1], interprete[LONINTERPRETE+1]; 34 char linea[MAXLINEA]; // Para evitar los problemas de scanf. 35 36 /* Inicializaci´on de la colecci´on. */ 37 mis_cds.cantidad = 0; 38 39 /* Bucle principal: men´u de opciones. */ 40 do { 41 do { 42 printf ("Colecci´on de CDsn"); 43 printf ("----------------n"); 44 printf ("1) A~nadir CDn"); 45 printf ("2) Listar todon"); 46 printf ("3) Listar por int´erpreten"); 47 printf ("4) Suprimir CDn"); 48 printf ("5) Salirn"); 49 printf ("Opci´on: "); 50 gets(linea); sscanf (linea, "%d", &opcion); 51 if (opcion <1 || opcion >5) 52 printf ("Opci´on inexistente. Debe estar entre 1 y 5n"); 53 } while (opcion <1 || opcion >5); 54 55 switch(opcion) { Introducci´on a la Programaci´on con C 133
  • 140. 2.4 Registros 56 case Anyadir: // A˜nadir un CD. 57 if (mis_cds.cantidad == MAXDISCOS) 58 printf ("La base de datos est´a llena. Lo siento.n"); 59 else { 60 printf ("T´ıtulo: "); 61 gets(mis_cds.cd[mis_cds.cantidad].titulo); 62 printf ("Int´erprete: "); 63 gets(mis_cds.cd[mis_cds.cantidad].interprete); 64 printf ("Minutos: "); 65 gets(linea); sscanf (linea, "%d", &mis_cds.cd[mis_cds.cantidad].duracion.minutos); 66 printf ("Segundos: "); 67 gets(linea); sscanf (linea, "%d", &mis_cds.cd[mis_cds.cantidad].duracion.segundos); 68 printf ("A~no: "); 69 gets(linea); sscanf (linea, "%d", &mis_cds.cd[mis_cds.cantidad].anyo); 70 mis_cds.cantidad++; 71 } 72 break; 73 74 case ListadoCompleto: // Listar todo. 75 for (i=0; i<mis_cds.cantidad; i++) 76 printf ("%d %s de %s (%d:%d) %dn", i, mis_cds.cd[i].titulo, 77 mis_cds.cd[i].interprete, 78 mis_cds.cd[i].duracion.minutos, 79 mis_cds.cd[i].duracion.segundos, 80 mis_cds.cd[i].anyo); 81 break; 82 83 case ListadoPorInterprete: // Listar por int´erprete. 84 printf ("Int´erprete: "); gets(interprete); 85 for (i=0; i<mis_cds.cantidad; i++) 86 if (strcmp(interprete, mis_cds.cd[i].interprete) == 0) 87 printf ("%d %s de %s (%d:%d) %dn", i, mis_cds.cd[i].titulo, 88 mis_cds.cd[i].interprete, 89 mis_cds.cd[i].duracion.minutos, 90 mis_cds.cd[i].duracion.segundos, 91 mis_cds.cd[i].anyo); 92 break; 93 94 case Suprimir: // Suprimir CD. 95 printf ("T´ıtulo: "); gets(titulo); 96 printf ("Int´erprete: "); gets(interprete); 97 for (i=0; i<mis_cds.cantidad; i++) 98 if (strcmp(titulo, mis_cds.cd[i].titulo) == 0 && 99 strcmp(interprete, mis_cds.cd[i].interprete) == 0) 100 break; 101 if (i < mis_cds.cantidad) { 102 for (j=i+1; j<mis_cds.cantidad; j++) 103 mis_cds.cd[j-1] = mis_cds.cd[j]; 104 mis_cds.cantidad--; 105 } 106 break; 107 } 108 109 } while (opcion != Salir); 110 printf ("Gracias por usar nuestro programa.n"); 111 112 return 0; 113 } En nuestro programa hemos separado la definici´on del tipo struct Coleccion de la declara- ci´on de la variable mis_cds. No es necesario. Podemos definir el tipo y declarar la variable en una sola sentencia: struct Coleccion { 134 Introducci´on a la Programaci´on con C
  • 141. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros struct CompactDisc cd[MAXDISCOS]; int cantidad; } mis_cds ; // Declara la variable mis_cds como de tipo struct Coleccion. Apuntemos ahora c´omo enriquecer nuestro programa de gesti´on de una colecci´on de discos compactos almacenando, adem´as, las canciones de cada disco. Empezaremos por definir un nuevo registro: el que modela una canci´on. De cada canci´on nos interesa el t´ıtulo, el autor y la duraci´on: 1 struct Cancion { 2 char titulo[LONTITULO+1]; 3 char autor[LONINTERPRETE+1]; 4 struct Tiempo duracion; 5 }; Hemos de modificar el registro struct CompactDisc para que almacene hasta, digamos, 20 canciones: 1 #define MAXCANCIONES 20 2 3 struct CompactDisc { 4 char titulo[LONTITULO+1]; 5 char interprete[LONINTERPRETE+1]; 6 struct Tiempo duracion; 7 int anyo; 8 struct Cancion cancion[MAXCANCIONES]; // Vector de canciones. 9 int canciones; // N´umero de canciones que realmente hay. 10 }; ¿C´omo leemos ahora un disco compacto? Aqu´ı tienes, convenientemente modificada, la por- ci´on del programa que se encarga de ello: 1 ... 2 int main(void) 3 { 4 int segundos; 5 ... 6 switch(opcion) { 7 case Anyadir: // A˜nadir un CD. 8 if (mis_cds.cantidad == MAXDISCOS) 9 printf ("La base de datos est´a llena. Lo siento.n"); 10 else { 11 printf ("T´ıtulo: "); 12 gets(mis_cds.cd[mis_cds.cantidad].titulo); 13 printf ("Int´erprete: "); 14 gets(mis_cds.cd[mis_cds.cantidad].interprete); 15 printf ("A~no: "); 16 gets(linea); sscanf (linea, "%d", &mis_cds.cd[mis_cds.cantidad].anyo); 17 18 do { 19 printf ("N´umero de canciones: "); 20 gets(linea); sscanf (linea, "%d", &mis_cds.cd[mis_cds.cantidad].canciones); 21 } while (mis_cds.cd[mis_cds.cantidad].canciones > MAXCANCIONES); 22 23 for (i=0; i<mis_cds.cd[mis_cds.cantidad].canciones; i++) { 24 printf ("T´ıtulo de la canci´on n´umero %d: ", i); 25 gets(mis_cds.cd[mis_cds.cantidad].cancion[i].titulo); 26 printf ("Autor de la canci´on n´umero %d: ", i); 27 gets(mis_cds.cd[mis_cds.cantidad].cancion[i].autor); 28 printf ("Minutos que dura la canci´on n´umero %d: ", i); 29 gets(linea); 30 sscanf (linea, "%d", &mis_cds.cd[mis_cds.cantidad].cancion[i].duracion.minutos); 31 printf ("y segundos: "); 32 gets(linea); 33 sscanf (linea, "%d", &mis_cds.cd[mis_cds.cantidad].cancion[i].duracion.segundos); 34 } Introducci´on a la Programaci´on con C 135
  • 142. 2.5 Definici´on de nuevos tipos de datos 35 36 segundos = 0; 37 for (i=0; i<mis_cds.cd[mis_cds.cantidad].canciones; i++) 38 segundos +=60 * mis_cds.cd[mis_cds.cantidad].cancion[i].duracion.minutos 39 + mis_cds.cd[mis_cds.cantidad].cancion[i].duracion.segundos; 40 mis_cds.cd[mis_cds.cantidad].duracion.minutos = segundos / 60; 41 mis_cds.cd[mis_cds.cantidad].duracion.segundos = segundos % 60; 42 43 mis_cds.cantidad++; 44 } 45 break; 46 ... 47 } Observa c´omo se calcula ahora la duraci´on del compacto como suma de las duraciones de todas sus canciones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 146 Dise˜na un programa C que gestione una agenda telef´onica. Cada entrada de la agenda contiene el nombre de una persona y hasta 10 n´umeros de tel´efono. El programa permitir´a a˜nadir nuevas entradas a la agenda y nuevos tel´efonos a una entrada ya existente. El men´u del programa permitir´a, adem´as, borrar entradas de la agenda, borrar n´umeros de tel´efono concretos de una entrada y efectuar b´usquedas por las primeras letras del nombre. (Si, por ejemplo, tu agenda contiene entradas para ((Jos´e Mart´ınez)), ((Josefa P´erez)) y ((Jaime Primero)), una b´usqueda por ((Jos)) mostrar´a a las dos primeras personas y una b´usqueda por ((J)) las mostrar´a a todas.) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.5. Definici´on de nuevos tipos de datos Los registros son nuevos tipos de datos cuyo nombre viene precedido por la palabra struct. C permite definir nuevos nombres para los tipos existentes con la palabra clave typedef. He aqu´ı un posible uso de typedef: 1 #define LONTITULO 80 2 #define LONINTERPRETE 40 3 4 struct Tiempo { 5 int minutos; 6 int segundos; 7 }; 8 9 typedef struct Tiempo TipoTiempo; 10 11 struct Cancion { 12 char titulo[LONTITULO+1]; 13 char autor[LONINTERPRETE+1]; 14 TipoTiempo duracion; 15 }; 16 17 typedef struct Cancion TipoCancion; 18 19 20 struct CompactDisc { 21 char titulo[LONTITULO+1]; 22 char interprete[LONINTERPRETE+1]; 23 TipoTiempo duracion; 24 int anyo; 25 TipoCancion cancion[MAXCANCIONES]; // Vector de canciones. 26 int canciones; // N´umero de canciones que realmente hay. 27 }; Hay una forma m´as compacta de definir un nuevo tipo a partir de un registro: 136 Introducci´on a la Programaci´on con C
  • 143. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores est´aticos y registros 1 #define LONTITULO 80 2 #define LONINTERPRETE 40 3 4 typedef struct { 5 int minutos; 6 int segundos; 7 } TipoTiempo; 8 9 typedef struct { 10 char titulo[LONTITULO+1]; 11 char autor[LONINTERPRETE+1]; 12 TipoTiempo duracion; 13 } TipoCancion; 14 15 typedef struct { 16 char titulo[LONTITULO+1]; 17 char interprete[LONINTERPRETE+1]; 18 TipoTiempo duracion; 19 int anyo; 20 TipoCancion cancion[MAXCANCIONES]; // Vector de canciones. 21 int canciones; // N´umero de canciones que realmente hay. 22 } TipoCompactDisc; 23 24 typedef struct { 25 TipoCompactDisc cd[MAXDISCOS]; 26 int cds; 27 } TipoColeccion; 28 29 int main(void) 30 { 31 TipoColeccion mis_cds; 32 ... Observa que, sistem´aticamente, hemos utilizado iniciales may´usculas para los nombres de tipos de datos (definidos con typedef y struct o s´olo con struct). Es un buen convenio para no confundir variables con tipos. Te recomendamos que hagas lo mismo o, en su defecto, que adoptes cualquier otro criterio, pero que sea coherente. El renombramiento de tipos no s´olo sirve para eliminar la molesta palabra clave struct, tambi´en permite dise˜nar programas m´as legibles y en los que resulta m´as f´acil cambiar tipos globalmente. Imagina que en un programa nuestro representamos la edad de una persona con un valor entre 0 y 127 (un char). Una variable edad se declarar´ıa as´ı: char edad; No es muy elegante: una edad no es un car´acter, sino un n´umero. Si definimos un ((nuevo)) tipo, el programa es m´as legible: typedef char TipoEdad; TipoEdad edad; Es m´as, si m´as adelante deseamos cambiar el tipo char por int, s´olo hemos de cambiar la l´ınea que empieza por typedef, aunque hayamos definido decenas de variables del tipo TipoEdad: typedef int TipoEdad; TipoEdad edad; Introducci´on a la Programaci´on con C 137
  • 144. 2.5 Definici´on de nuevos tipos de datos Los cambios de tipos y sus consecuencias Te hemos dicho que typedef permite definir nuevos tipos y facilita sustituir un tipo por otro en diferentes versiones de un mismo programa. Es cierto, pero problem´atico. Imagina que en una aplicaci´on definimos un tipo edad como un car´acter sin signo y que definimos una variable de dicho tipo cuyo valor leemos de teclado: 1 #include <stdio.h> 2 3 typedef unsigned char TipoEdad; 4 5 int main(void) 6 { 7 TipoEdad mi_edad; 8 9 printf ("Introduzca edad: "); 10 scanf ("%hhu", &mi_edad); 11 printf ("Valor le´ıdo %hhun", mi_edad); 12 13 return 0; 14 } ¿Qu´e pasa si, posteriormente, decidimos que el tipo TipoEdad debiera ser un entero de 32 bits? He aqu´ı una versi´on err´onea del programa: 1 #include <stdio.h> 2 3 typedef int TipoEdad; 4 5 int main(void) 6 { 7 TipoEdad mi_edad; 8 9 printf ("Introduzca edad: "); 10 scanf ("%hhu", &mi_edad); // ! Mal! 11 printf ("Valor le´ıdo %hhun", mi_edad); // ! Mal! 12 13 return 0; 14 } ¿Y por qu´e es err´oneo? Porque debi´eramos haber modificado adem´as las marcas de formato de scanf y printf : en lugar de %hhu deber´ıamos usar ahora %d. C no es un lenguaje id´oneo para este tipo de modificaciones. Otros lenguajes, como C++ soportan de forma mucho m´as flexible la posibilidad de cambiar tipos de datos, ya que no obligan al programador a modificar un gran n´umero de l´ıneas del programa. 138 Introducci´on a la Programaci´on con C
  • 145. Cap´ıtulo 3 Funciones Un momento despu´es, Alicia atravesaba el cristal, y saltaba ´agilmente a la habitaci´on del Espejo. Lewis Carroll, Alicia a trav´es del espejo. Vamos a estudiar la definici´on y uso de funciones en C. El concepto es el mismo que ya estudiaste al aprender Python: una funci´on es un fragmento de programa parametrizado que efect´ua unos c´alculos y, o devuelve un valor como resultado, o tiene efectos laterales (modificaci´on de variables globales o argumentos, volcado de informaci´on en pantalla, etc.), o ambas cosas. La principal diferencia entre Python y C estriba en el paso de par´ametros. En este aspecto, C presenta ciertas limitaciones frente a Python, pero tambi´en ciertas ventajas. Entre las limitaciones tenemos la necesidad de dar un tipo a cada par´ametro y al valor de retorno, y entre las ventajas, la posibilidad de pasar variables escalares y modificar su valor en el cuerpo de la funci´on (gracias al uso de punteros). Estudiaremos tambi´en la posibilidad de declarar y usar variables locales, y volveremos a tratar la recursividad. Adem´as, veremos c´omo implementar nuestros propios m´odulos mediante las denominadas unidades de compilaci´on y la creaci´on de ficheros de cabecera. Finalmente, estudiaremos la definici´on y el uso de macros, una especie de ((pseudo-funciones)) que gestiona el preprocesador de C. 3.1. Definici´on de funciones En C no hay una palabra reservada (como def en Python) para iniciar la definici´on de una funci´on. El aspecto de una definici´on de funci´on en C es ´este: 1 tipo_de_retorno identificador ( par´ametros ) 2 { 3 cuerpo_de_la_funci´on 4 } El cuerpo de la funci´on puede contener declaraciones de variables locales (t´ıpicamente en sus primeras l´ıneas). Aqu´ı tienes un ejemplo de definici´on de funci´on: una funci´on que calcula el logaritmo en base b (para b entero) de un n´umero x. La hemos definido de un modo menos compacto de lo que podemos hacer para ilustrar los diferentes elementos que puedes encontrar en una funci´on: 1 float logaritmo (float x, int b) 2 { 3 float logbase, resultado; 4 5 logbase = log10(b); 6 resultado = log10(x)/logbase; 7 return resultado; 8 } Deteng´amonos a analizar brevemente cada uno de los componentes de la definici´on de una funci´on e identifiqu´emoslos en el ejemplo: Introducci´on a la Programaci´on con C 139
  • 146. 3.1 Definici´on de funciones El tipo de retorno indica de qu´e tipo de datos es el valor devuelto por la funci´on como resultado (m´as adelante veremos c´omo definir procedimientos, es decir, funciones sin valor de retorno). Puedes considerar esto como una limitaci´on frente a Python: en C, cada funci´on devuelve valores de un ´unico tipo. No podemos definir una funci´on que, seg´un convenga, devuelva un entero, un flotante o una cadena, como hicimos en Python cuando nos convino. En nuestro ejemplo, la funci´on devuelve un valor de tipo float. 1 float logaritmo (float x, int b) 2 { 3 float logbase, resultado ; 4 5 logbase = log10(b); 6 resultado = log10(x)/logbase; 7 return resultado ; 8 } El identificador es el nombre de la funci´on y, para estar bien formado, debe observar las mismas reglas que se siguen para construir nombres de variables. Eso s´ı, no puedes definir una funci´on con un identificador que ya hayas usado para una variable (u otra funci´on). El identificador de nuestra funci´on de ejemplo es logaritmo: 1 float logaritmo (float x, int b) 2 { 3 float logbase, resultado; 4 5 logbase = log10(b); 6 resultado = log10(x)/logbase; 7 return resultado; 8 } Entre par´entesis aparece una lista de declaraciones de par´ametros separadas por comas. Cada declaraci´on de par´ametro indica tanto el tipo del mismo como su identificador1 . Nuestra funci´on tiene dos par´ametros, uno de tipo float y otro de tipo int. 1 float logaritmo (float x, int b) 2 { 3 float logbase, resultado; 4 5 logbase = log10(b); 6 resultado = log10(x)/logbase; 7 return resultado; 8 } El cuerpo de la funci´on debe ir encerrado entre llaves, aunque s´olo conste de una sentencia. Puede empezar por una declaraci´on de variables locales a la que sigue una o m´as sentencias C. La sentencia return permite finalizar la ejecuci´on de la funci´on y devolver un valor (que debe ser del mismo tipo que el indicado como tipo de retorno). Si no hay sentencia return, la ejecuci´on de la funci´on finaliza tambi´en al acabar de ejecutar la ´ultima de las sentencias de su cuerpo, pero es un error no devolver nada con return si se ha declarado la funci´on como tal, y no como procedimiento. Nuestra funci´on de ejemplo tiene un cuerpo muy sencillo. Hay una declaraci´on de variables (locales) y est´a formado por tres sentencias, dos de asignaci´on y una de devoluci´on de valor: 1 float logaritmo (float x, int b) 2 { 3 float logbase, resultado; 4 1Eso en el caso de par´ametros escalares. Los par´ametros de tipo vectorial se estudiar´an m´as adelante. 140 Introducci´on a la Programaci´on con C
  • 147. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones 5 logbase = log10(b); 6 resultado = log10(x)/logbase; 7 return resultado; 8 } La sentencia (o sentencias) de devoluci´on de valor forma(n) parte del cuerpo y empieza(n) con la palabra return. Una funci´on puede incluir m´as de una sentencia de devoluci´on de valor, pero debes tener en cuenta que la ejecuci´on de la funci´on finaliza con la primera ejecuci´on de una sentencia return. 1 float logaritmo (float x, int b) 2 { 3 float logbase, resultado; 4 5 logbase = log10(b); 6 resultado = log10(x)/logbase; 7 return resultado; 8 } La funci´on logaritmo se invoca como una funci´on cualquiera de math.h: logaritmo.c logaritmo.c 1 #include <stdio.h> 2 #include <math.h> 3 4 float logaritmo (float x, int b) 5 { 6 float logbase, resultado; 7 8 logbase = log10(b); 9 resultado = log10(x)/logbase; 10 return resultado; 11 } 12 13 int main (void) 14 { 15 float y; 16 17 y = logaritmo(128.0, 2); 18 printf ("%fn", y); 19 20 return 0; 21 } Si ejecutamos el programa tenemos: 7.000000 Es necesario que toda funci´on se defina en el programa antes de la primera l´ınea en que se usa. Por esta raz´on, todas nuestras funciones se definen delante de la funci´on main, que es la funci´on que contiene el programa principal y a la que, por tanto, no se llama desde ning´un punto del programa.2 Naturalmente, ha resultado necesario incluir la cabecera math.h en el programa, ya que usamos la funci´on log10. Recuerda, adem´as, que al compilar se debe enlazar con la biblioteca matem´atica, es decir, se debe usar la opci´on -lm de gcc. Esta ilustraci´on te servir´a para identificar los diferentes elementos de la definici´on de una funci´on y de su invocaci´on: 2Nuevamente hemos de matizar una afirmaci´on: en realidad s´olo es necesario que se haya declarado el prototipo de la funci´on. M´as adelante daremos m´as detalles. Introducci´on a la Programaci´on con C 141
  • 148. 3.1 Definici´on de funciones float logaritmo (float x, int b) { float logbase, resultado; logbase = log10(b); resultado = log10(x)/logbase; return resultado; } ... int main(void) { float y; ... y = logaritmo( 128.0, 2 ); ... Par´ametros formales (o simplemente par´ametros) Tipo de retorno Identificador Cabecera Cuerpo Declaraci´on de variables locales Sentencia de devoluci´on de valor Llamada, invocaci´on o activaci´on Argumentos o par´ametros reales Identificador ¡Ah! Te hemos dicho antes que la funci´on logaritmo no es muy compacta. Podr´ıamos haberla definido as´ı: float logaritmo (float x, int b) { return log10(x)/log10(b); } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 147 Define una funci´on que reciba un int y devuelva su cuadrado. · 148 Define una funci´on que reciba un float y devuelva su cuadrado. · 149 Define una funci´on que reciba dos float y devuelva 1 (((cierto))) si el primero es menor que el segundo y 0 (((falso))) en caso contrario. · 150 Define una funci´on que calcule el volumen de una esfera a partir de su radio r. (Recuerda que el volumen de una esfera de radio r es 4/3πr3 .) · 151 El seno hiperb´olico de x es sinh = ex − e−x 2 . Dise˜na una funci´on C que efect´ue el calculo de senos hiperb´olicos. (Recuerda que ex se puede calcular con la funci´on exp, disponible incluyendo math.h y enlazando el programa ejecutable con la librer´ıa matem´atica.) · 152 Dise˜na una funci´on que devuelva ((cierto)) (el valor 1) si el a˜no que se le suministra como argumento es bisiesto, y ((falso)) (el valor 0) en caso contrario. · 153 La distancia de un punto (x0, y0) a una recta Ax + By + C = 0 viene dada por d = Ax0 + By0 + C √ A2 + B2 . Dise˜na una funci´on que reciba los valores que definen una recta y los valores que definen un punto y devuelva la distancia del punto a la recta. 142 Introducci´on a la Programaci´on con C
  • 149. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Veamos otro ejemplo de definici´on de funci´on: 1 int minimo(int a, int b, int c) 2 { 3 if (a <= b) 4 if (a <= c) 5 return a; 6 else 7 return c; 8 else 9 if (b <= c) 10 return b; 11 else 12 return c; 13 } La funci´on minimo devuelve un dato de tipo int y recibe tres datos, tambi´en de tipo int. No hay problema en que aparezca m´as de una sentencia return en una funci´on. El comportamiento de return es el mismo que estudiamos en Python: tan pronto se ejecuta, finaliza la ejecuci´on de la funci´on y se devuelve el valor indicado. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 154 Define una funci´on que, dada una letra min´uscula del alfabeto ingl´es, devuelva su correspondiente letra may´uscula. Si el car´acter recibido como dato no es una letra min´uscula, la funci´on la devolver´a inalterada. · 155 ¿Qu´e error encuentras en esta funci´on? 1 int minimo (int a, b, c) 2 { 3 if (a <= b && a <= c) 4 return a; 5 if (b <= a && b <= c) 6 return b; 7 return c; 8 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Observa que main es una funci´on. Su cabecera es int main(void). ¿Qu´e significa void? Significa que no hay par´ametros. Pero no nos adelantemos. En este mismo cap´ıtulo hablaremos de funciones sin par´ametros. 3.2. Variables locales y globales Cada funci´on puede definir sus propias variables locales defini´endolas en su cuerpo. C permite, adem´as, definir variables fuera del cuerpo de cualquier funci´on: son las variables globales. 3.2.1. Variables locales Las variables que declaramos justo al principio del cuerpo de una funci´on son variables locales. Este programa, por ejemplo, declara dos variables locales para calcular el sumatorio b i=a i. La variable local a sumatorio con identificador i nada tiene que ver con la variable del mismo nombre que es local a main: locales.c locales.c 1 #include <stdio.h> 2 3 int sumatorio(int a, int b) 4 { 5 int i, s; // Variables locales a sumatorio. 6 7 s = 0; Introducci´on a la Programaci´on con C 143
  • 150. 3.2 Variables locales y globales 8 for (i=a; i<=b; i++) 9 s += i; 10 return s; 11 } 12 13 int main(void) 14 { 15 int i; // Variable local a main. 16 17 for (i=1; i<=10; i++) 18 printf ("Sumatorio de los %d primeros n´umeros naturales: %dn", i, sumatorio(1, i)); 19 return 0; 20 } Las variables locales i y s de sumatorio s´olo ((viven)) durante las llamadas a sumatorio. La zona en la que es visible una variable es su ´ambito. Las variables locales s´olo son visibles en el cuerpo de la funci´on en la que se declaran; ´ese es su ´ambito. Variables locales a bloques El concepto de variable local no est´a limitado, en C, a las funciones. En realidad, puedes definir variables locales en cualquier bloque de un programa. F´ıjate en este ejemplo: 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int i; 6 7 for (i=0; i<3; i++) { 8 int j; 9 for (j=0; j<3; j++) 10 printf ("%d-%d ", i, j); 11 printf ("n"); 12 } 13 return 0; 14 } La variable j s´olo existe en el bloque en el que se ha declarado, es decir, en la zona sombreada. Ese es su ´ambito. La variable i tiene un ´ambito que engloba al de j. Puedes comprobar, pues, que una variable local a una funci´on es tambi´en una variable local a un bloque: s´olo existe en el bloque que corresponde al cuerpo de la funci´on. Como ya te dijimos en un cuadro del cap´ıtulo 1, C99 permite declarar variables de ´ındice de bucle de usar y tirar. Su ´ambito se limita al bucle. Aqu´ı tienes un ejemplo en el que hemos sombreado el ´ambito de la variable j: 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int i; 6 7 for (i=0; i<3; i++) { 8 for (int j=0; j<3; j++) 9 printf ("%d-%d ", i, j); 10 printf ("n"); 11 } 12 return 0; 13 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 156 Dise˜na una funci´on que calcule el factorial de un entero n. 144 Introducci´on a la Programaci´on con C
  • 151. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones · 157 Dise˜na una funci´on que calcule xn , para n entero y x de tipo float. (Recuerda que si n es negativo, xn es el resultado de multiplicar 1/x por s´ı mismo −n veces.) · 158 El valor de la funci´on ex puede aproximarse con el desarrollo de Taylor: ex ≈ 1 + x + x2 2! + x3 3! + x4 4! + · · · Dise˜na una funci´on que aproxime el valor de ex usando n t´erminos del desarrollo de Taylor, siendo n un n´umero entero positivo. (Puedes usar, si te conviene, la funci´on de exponenciaci´on del ´ultimo ejercicio para calcular los distintos valores de xi , aunque hay formas m´as eficientes de calcular x/1!, x2 /2!, x3 /3!, . . . , ¿sabes c´omo? Plant´eate c´omo generar un t´ermino de la forma xi /i! a partir de un t´ermino de la forma xi−1 /(i − 1)!.) · 159 El valor de la funci´on coseno puede aproximarse con el desarrollo de Taylor: cos(x) ≈ 1 − x2 2! + x4 4! − x6 6! + · · · Dise˜na una funci´on que aproxime el coseno de un valor x usando n t´erminos del desarrollo de Taylor, siendo n un n´umero entero positivo. · 160 Dise˜na una funci´on que diga si un n´umero es perfecto o no. Si el n´umero es perfecto, devolver´a ((cierto)) (el valor 1) y si no, devolver´a ((falso)) (el valor 0). Un n´umero es perfecto si es igual a la suma de todos sus divisores (excepto ´el mismo). · 161 Dise˜na una funci´on que diga si un n´umero entero es o no es capic´ua. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.2. Variables globales Las variables globales se declaran fuera del cuerpo de cualquier funci´on y son accesibles desde cualquier punto del programa posterior a su declaraci´on. Este fragmento de programa, por ejemplo, define una variable global i y una variable local a main con el mismo identificador: globales.c globales.c 1 #include <stdio.h> 2 3 int i = 1; // Variable global i. 4 5 int doble(void) 6 { 7 i *= 2; // Referencia a la variable global i. 8 return i; // Referencia a la variable global i. 9 } 10 11 int main(void) 12 { 13 int i; // Variable local i. 14 15 for (i=0; i<5; i++) // Referencias a la variable local i. 16 printf ("%dn", doble()); // Ojo: el valor mostrado corresponde a la i global. 17 18 return 0; 19 } F´ıjate en la p´erdida de legibilidad que supone el uso del identificador i en diferentes puntos del programa: hemos de preguntarnos siempre si corresponde a la variable local o global. Te desaconsejamos el uso generalizado de variables globales en tus programas. Como evitan usar par´ametros en funciones, llegan a resultar muy c´omodas y es f´acil que abuses de ellas. No es que siempre se usen mal, pero se requiere una cierta experiencia para formarse un criterio firme que permita decidir cu´ando resulta conveniente usar una variable global y cu´ando conviene suministrar informaci´on a funciones mediante par´ametros. Como estudiante te pueden parecer un recurso c´omodo para evitar suministrar informaci´on a las funciones mediante par´ametros. Ese peque˜no beneficio inmediato es, cr´eenos, un lastre a Introducci´on a la Programaci´on con C 145
  • 152. 3.3 Funciones sin par´ametros medio y largo plazo: aumentar´a la probabilidad de que cometas errores al intentar acceder o modificar una variable y las funciones que definas en un programa ser´an dif´ıcilmente reutilizables en otros. Est´as aprendiendo a programar y pretendemos evitar que adquieras ciertos vicios, as´ı que te prohibimos que las uses. . . salvo cuando convenga que lo hagas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 162 ¿Qu´e muestra por pantalla este programa? contador global.c contador global.c 1 #include <stdio.h> 2 3 int contador ; // Variable global. 4 5 void fija(int a) 6 { 7 contador = a; 8 } 9 10 int decrementa(int a) 11 { 12 contador -= a; 13 return contador ; 14 } 15 16 void muestra(int contador) 17 { 18 printf ("[%d]n", contador); 19 } 20 21 void cuenta_atras(int a) 22 { 23 int contador; 24 for (contador=a; contador >=0; contador--) 25 printf ("%d ", contador); 26 printf ("n"); 27 } 28 29 int main(void) { 30 int i; 31 32 contador = 10; 33 i = 1; 34 while (contador >= 0) { 35 muestra(contador); 36 cuenta_atras(contador); 37 muestra(i); 38 decrementa(i); 39 i *= 2; 40 } 41 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3. Funciones sin par´ametros Puedes definir una funci´on sin par´ametros dejando la palabra void como contenido de la lista de par´ametros. Esta funci´on definida por nosotros, por ejemplo, utiliza la funci´on rand de stdlib.h para devolver un n´umero aleatorio entre 1 y 6: int dado (void) { return rand() % 6 + 1; } 146 Introducci´on a la Programaci´on con C
  • 153. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones Para llamar a la funci´on dado hemos de a˜nadir un par de par´entesis a la derecha del iden- tificador, aunque no tenga par´ametros. Ya te hab´ıamos anticipado que la funci´on main es una funci´on sin par´ametros que devuelve un entero: dado.c dado.c 1 #include <stdio.h> 2 #include <stdlib.h> 3 4 int dado(void) 5 { 6 return rand() % 6 + 1; 7 } 8 9 int main(void) 10 { 11 int i; 12 for (i=0; i<10; i++) 13 printf ("%dn", dado() ); 14 return 0; 15 } ((Calidad)) de los n´umeros aleatorios La funci´on rand est´a pobremente implementada en muchas m´aquinas y genera patrones repetitivos que hacen poco aleatoria la secuencia de n´umeros generada. Este problema se agudiza si observamos los bits menos significativos de los n´umeros generados. . . ¡y eso es, precisamente, lo que estamos haciendo con expresiones como rand() % 6! Una forma de paliar este problema es usar una expresi´on diferente: int dado(void) { return (int) ((double) rand() / ((double) RAND_MAX + 1) * 6) + 1; } La constante RAND_MAX es el mayor n´umero aleatorio que puede devolver rand. La divisi´on hace que el n´umero generado est´e en el intervalo [0, 1[. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 163 El programa dado.c siempre genera la misma secuencia de n´umeros aleatorios. Para evitarlo, debes proporcionar una semilla diferente con cada ejecuci´on del programa. El valor de la semilla se suministra como ´unico argumento de la funci´on srand. Modifica dado.c para que solicite al usuario la introducci´on del valor semilla. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Un uso t´ıpico de las funciones sin par´ametros es la lectura de datos por teclado que deben satisfacer una serie de restricciones. Esta funci´on, por ejemplo, lee un n´umero entero de teclado y se asegura de que sea par: 1 int lee_entero_par(void) 2 { 3 int numero; 4 5 scanf ("%d", &numero); 6 while (numero % 2 != 0) { 7 printf ("El n´umero debe ser par y %d no lo es.n", numero); 8 numero = scanf ("%d", &numero); 9 } 10 return numero; 11 } Otro uso t´ıpico es la presentaci´on de men´us de usuario con lectura de la opci´on seleccionada por el usuario: Introducci´on a la Programaci´on con C 147
  • 154. 3.4 Procedimientos 1 int menu_principal(void) 2 { 3 int opcion; 4 5 do { 6 printf ("1) Alta usuarion"); 7 printf ("2) Baja usuarion"); 8 printf ("3) Consulta usuarion"); 9 printf ("4) Salirn"); 10 11 printf ("Opci´on: "); scanf ("%d", &opcion); 12 if (opcion < 1 || opcion > 4) 13 printf ("Opci´on no v´alida.n"); 14 } while (opcion < 1 || opcion > 4); 15 16 return opcion; 17 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 164 Dise˜na una funci´on que lea por teclado un entero positivo y devuelva el valor le´ıdo. Si el usuario introduce un n´umero negativo, la funci´on advertir´a del error por pantalla y leer´a nuevamente el n´umero cuantas veces sea menester. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4. Procedimientos Un procedimiento, como recordar´as, es una funci´on que no devuelve valor alguno. Los procedi- mientos provocan efectos laterales, como imprimir un mensaje por pantalla, modificar variables globales o modificar el valor de sus par´ametros. Los procedimientos C se declaran como funciones con tipo de retorno void. Mira este ejem- plo: 1 #include <stdio.h> 2 3 void saludos(void) 4 { 5 printf ("Hola, mundo.n"); 6 } En un procedimiento puedes utilizar la sentencia return, pero sin devolver valor alguno. Cuando se ejecuta una sentencia return, finaliza inmediatamente la ejecuci´on del procedimien- to. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 165 Dise˜na un procedimiento que reciba un entero n y muestre por pantalla n asteriscos seguidos con un salto de l´ınea al final. · 166 Dise˜na un procedimiento que, dado un valor de n, dibuje con asteriscos un tri´angulo rect´angulo cuyos catetos midan n caracteres. Si n es 5, por ejemplo, el procedimiento mostrar´a por pantalla este texto: 1 * 2 ** 3 *** 4 **** 5 ***** Puedes usar, si te conviene, el procedimiento desarrollado en el ejercicio anterior. · 167 Dise˜na un procedimiento que reciba un n´umero entero entre 0 y 99 y muestre por pantalla su trascripci´on escrita. Si le suministramos, por ejemplo, el valor 31, mostrar´a el texto ((treinta y uno)). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148 Introducci´on a la Programaci´on con C
  • 155. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones 3.5. Paso de par´ametros 3.5.1. Par´ametros escalares: paso por valor Aunque modifiques el valor de un par´ametro escalar en una funci´on o procedimiento, el valor de la variable pasada como argumento permanece inalterado. La funci´on bits del siguiente programa, por ejemplo, modifica en su cuerpo el valor de su par´ametro num: numbits.c numbits.c 1 #include <stdio.h> 2 3 int bits(unsigned int num ) 4 { 5 int b = 0; 6 7 do { 8 b++; 9 num /= 2; 10 } while (num > 0); 11 12 return b; 13 } 14 15 int main(void) 16 { 17 unsigned int numero ; 18 int bitsnumero; 19 20 printf ("Introduce un entero positivo: "); scanf ("%u", &numero ); 21 bitsnumero = bits(numero ); 22 printf ("Hay %d bits en %u.n", bitsnumero, numero ); 23 return 0; 24 } Al ejecutar el programa y teclear el n´umero 128 se muestra por pantalla lo siguiente: Introduce un entero positivo: 128 Hay 8 bits en 128. Como puedes ver, el valor de numero permanece inalterado tras la llamada a bits, aunque en el cuerpo de la funci´on se modifica el valor del par´ametro num (que toma el valor de numero en la llamada). Un par´ametro es como una variable local, s´olo que su valor inicial se obtiene copiando el valor del argumento que suministramos. As´ı pues, num no es numero, sino otra variable que contiene una copia del valor de numero. Es lo que se denomina paso de par´ametro por valor. Llegados a este punto conviene que nos detengamos a estudiar c´omo se gestiona la memoria en las llamadas a funci´on. 3.5.2. Organizaci´on de la memoria: la pila de llamadas a funci´on En C las variables locales se gestionan, al igual que en Python, mediante una pila. Cada funci´on activada se representa en la pila mediante un registro de activaci´on o trama de activaci´on. Se trata de una zona de memoria en la que se almacenan las variables locales y par´ametros junto a otra informaci´on, como el punto desde el que se llam´o a la funci´on. Cuando iniciamos la ejecuci´on del programa numbits.c, se activa autom´aticamente la fun- ci´on main. En ´ella tenemos dos variables locales: numero y bitsnumero. main numero bitsnumero Introducci´on a la Programaci´on con C 149
  • 156. 3.5 Paso de par´ametros Si el usuario teclea el valor 128, ´este se almacena en numero: main 128numero bitsnumero Cuando se produce la llamada a la funci´on bits, se crea una nueva trama de activaci´on: llamada desde l´ınea 21 main 128numero bitsnumero bits num b El par´ametro num recibe una copia del contenido de numero y se inicializa la variable local b con el valor 0: llamada desde l´ınea 21 main 128numero bitsnumero bits 128num 0b Tras ejecutar el bucle de bits, la variable b vale 8. Observa que aunque num ha modificado su valor y ´este proven´ıa originalmente de numero, el valor de numero no se altera: llamada desde l´ınea 21 main 128numero bitsnumero bits 0num 8b La trama de activaci´on de bits desaparece ahora, pero dejando constancia del valor devuelto por la funci´on: main 128numero bitsnumero 8return Y, finalmente, el valor devuelto se copia en bitsnumero: main 128numero 8bitsnumero 150 Introducci´on a la Programaci´on con C
  • 157. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones Como ves, las variables locales s´olo ((viven)) durante la ejecuci´on de cada funci´on. C obtiene una copia del valor de cada par´ametro y la deja en la pila. Cuando modificamos el valor de un par´ametro en el cuerpo de la funci´on, estamos modificando el valor del argumento, no el de la variable original. Este otro programa declara numero como una variable global y trabaja directamente con dicha variable: numbits2.c numbits2.c 1 #include <stdio.h> 2 3 unsigned int numero; 4 5 int bits(void) 6 { 7 int b = 0; 8 9 do { 10 b++; 11 numero /= 2; 12 } while (numero > 0); 13 14 return b; 15 } 16 17 int main(void) 18 { 19 int bitsnumero; 20 21 printf ("Introduce un entero positivo: "); scanf ("%u", &numero ); 22 bitsnumero = bits(); 23 printf ("Hay %d bits, pero ahora ’numero’ vale %u.n", bitsnumero, numero); 24 return 0; 25 } Las variables globales residen en una zona especial de la memoria y son accesibles desde cual- quier funci´on. Representaremos dicha zona como un ´area enmarcada con una l´ınea discont´ınua. Cuando se inicia la ejecuci´on del programa, ´esta es la situaci´on: main bitsnumero variables globales numero En main se da valor a la variable global numero: main bitsnumero variables globales 128numero Y se llama a continuaci´on a bits sin argumento alguno: bits b llamada desde l´ınea 22 main bitsnumero variables globales 128numero Introducci´on a la Programaci´on con C 151
  • 158. 3.5 Paso de par´ametros El c´alculo de bits modifica el valor de numero. Tras la primera iteraci´on del bucle while, ´esta es la situaci´on: bits 1b llamada desde l´ınea 22 main bitsnumero variables globales 64numero Cuando finaliza la ejecuci´on de bits tenemos: 8return main bitsnumero variables globales 0numero Entonces se copia el valor devuelto en bitsnumero: main 8bitsnumero variables globales 0numero El mensaje que obtenemos en pantalla es: Introduce un entero positivo: 128 Hay 8 bits, pero ahora ’numero’ vale 0. Bueno. Ahora sabes qu´e pasa con las variables globales y c´omo acceder a ellas desde las funciones. Pero repetimos lo que te dijimos al aprender Python: pocas veces est´a justificado acceder a variables globales, especialmente cuando est´as aprendiendo. Ev´ıtalas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 168 Estudia este programa y muestra gr´aficamente el contenido de la memoria cuando se van a ejecutar por primera vez las l´ıneas 24, 14 y 5. suma cuadrados.c suma cuadrados.c 1 #include <stdio.h> 2 3 int cuadrado(int i) 4 { 5 return i * i; 6 } 7 8 int sumatorio(int a, int b) 9 { 10 int i, s; 11 12 s = 0; 13 for (i=a; i<=b; i++) 14 s += cuadrado(i); 15 return s; 16 } 17 18 int main(void) 19 { 20 int i, j; 21 22 i = 10; 152 Introducci´on a la Programaci´on con C
  • 159. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones 23 j = 20; 24 printf ("%dn", sumatorio(i, j)); 25 return 0; 26 } · 169 Este programa muestra por pantalla los 10 primeros n´umeros primos. La funci´on si- guiente genera cada vez un n´umero primo distinto. Gracias a la variable global ultimoprimo la funci´on ((recuerda)) cu´al fue el ´ultimo n´umero primo generado. Haz una traza paso a paso del programa (hasta que haya generado los 4 primeros primos). Muestra el estado de la pila y el de la zona de variables globales en los instantes en que se llama a la funci´on siguienteprimo y cuando ´esta devuelve su resultado diez primos.c diez primos.c 1 #include <stdio.h> 2 3 int ultimoprimo = 0; 4 5 int siguienteprimo(void) 6 { 7 int esprimo , i; 8 9 do { 10 ultimoprimo++; 11 esprimo = 1; 12 for (i=2; i<ultimoprimo/2; i++) 13 if (ultimoprimo % i == 0) { 14 esprimo = 0; 15 break; 16 } 17 } while (!esprimo); 18 return ultimoprimo; 19 } 20 21 int main(void) 22 { 23 int i; 24 25 printf ("Los 10 primeros n´umeros primosn"); 26 for (i=0; i<10; i++) 27 printf ("%dn", siguienteprimo()); 28 return 0; 29 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . No hay problema con que las variables locales a una funci´on sean vectores. Su contenido se almacena siempre en la pila. Este programa, por ejemplo, cuenta la cantidad de n´umeros primos entre 1 y el valor que se le indique (siempre que no supere cierta constante N) con la ayuda de la criba de Erat´ostenes. El vector con el que se efect´ua la criba es una variable local a la funci´on que efect´ua el conteo: eratostenes 1.c eratostenes.c 1 #include <stdio.h> 2 3 #define N 10 4 5 int cuenta_primos(int n) //Cuenta el n´umero de primos entre 1 y n. 6 { 7 char criba[N]; 8 int i, j, numprimos; 9 10 /* Comprobemos que el argumento es v´alido */ 11 if (n >= N) 12 return -1; // Devolvemos −1 si no es v´alido. 13 14 /* Inicializaci´on */ Introducci´on a la Programaci´on con C 153
  • 160. 3.5 Paso de par´ametros 15 criba[0] = 0; 16 for (i=1; i<n; i++) 17 criba[i] = 1; 18 19 /* Criba de Erat´ostenes */ 20 for (i=2; i<n; i++) 21 if (criba[i]) 22 for (j=2; i*j<n; j++) 23 criba[i*j] = 0; 24 25 /* Conteo de primos */ 26 numprimos = 0; 27 for (i=0; i<n; i++) 28 if (criba[i]) 29 numprimos++; 30 31 return numprimos; 32 } 33 34 35 int main(void) 36 { 37 int hasta, cantidad; 38 39 printf ("Introduce un valor:"); scanf ("%d", &hasta); 40 cantidad = cuenta_primos(hasta); 41 if (cantidad == -1) 42 printf ("No puedo efectuar ese c´alculo. El mayor valor permitido es %d.n", N-1); 43 else 44 printf ("Primos entre 1 y %d: %dn", hasta, cantidad); 45 return 0; 46 } Cuando el programa inicia su ejecuci´on, se crea una trama de activaci´on en la que se albergan las variables hasta y cantidad. Supongamos que cuando se solicita el valor de hasta el usuario introduce el valor 6. He aqu´ı el aspecto de la memoria: main cantidad 6hasta Se efect´ua entonces (l´ınea 40) la llamada a cuenta_primos, con lo que se crea una nueva tra- ma de activaci´on. En ella se reserva memoria para todas las variables locales de cuenta_primos: llamada desde l´ınea 40 main cuenta primos cantidad 6hasta numprimos i j criba 0 1 2 3 4 5 6 7 8 9 n 154 Introducci´on a la Programaci´on con C
  • 161. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones Observa que el vector criba ocupa memoria en la propia trama de activaci´on. Completa t´u mismo el resto de acciones ejecutadas por el programa ayud´andote de una traza de la pila de llamadas a funci´on con gr´aficos como los mostrados. 3.5.3. Vectores de longitud variable Te hemos dicho en el apartado 2.1 que los vectores han de tener talla conocida en tiempo de compilaci´on. Es hora de matizar esa afirmaci´on. Los vectores locales a una funci´on pueden determinar su talla en tiempo de ejecuci´on. Veamos un ejemplo: eratostenes 2.c eratostenes.c 1 #include <stdio.h> 2 3 int cuenta_primos(int n) //Cuenta el n´umero de primos entre 1 y n. 4 { 5 char criba[n]; 6 int i, j, numprimos; 7 8 /* Inicializaci´on */ 9 criba[0] = 0; 10 for (i=1; i<n; i++) 11 criba[i] = 1; 12 13 /* Criba de Erat´ostenes */ 14 for (i=2; i<n; i++) 15 if (criba[i]) 16 for (j=2; i*j<n; j++) 17 criba[i*j] = 0; 18 19 /* Conteo de primos */ 20 numprimos = 0; 21 for (i=0; i<n; i++) 22 if (criba[i]) 23 numprimos++; 24 25 return numprimos; 26 } 27 28 29 int main(void) 30 { 31 int hasta, cantidad; 32 33 printf ("Introduce un valor:"); scanf ("%d", &hasta); 34 cantidad = cuenta_primos(hasta); 35 printf ("Primos entre 1 y %d: %dn", hasta, cantidad); 36 return 0; 37 } F´ıjate en c´omo hemos definido el vector criba: la talla no es un valor constante, sino n, un par´ametro cuyo valor es desconocido hasta el momento en que se ejecute la funci´on. Esta es una caracter´ıstica de C99 y supone una mejora interesante del lenguaje. 3.5.4. Par´ametros vectoriales: paso por referencia Este programa ilustra el modo en que podemos declarar y pasar par´ametros vectoriales a una funci´on: pasa vector.c pasa vector.c 1 #include <stdio.h> 2 3 #define TALLA 3 Introducci´on a la Programaci´on con C 155
  • 162. 3.5 Paso de par´ametros 4 5 void incrementa(int a[]) 6 { 7 int i; 8 9 for (i=0; i<TALLA; i++) 10 a[i]++; 11 } 12 13 int main(void) 14 { 15 int i, v[TALLA]; 16 17 18 printf ("Al principio:n"); 19 for (i=0; i<TALLA; i++) { 20 v[i] = i; 21 printf ("%d: %dn", i, v[i]); 22 } 23 incrementa(v); 24 printf ("Despu´es de llamar a incrementa:n"); 25 for (i=0; i<TALLA; i++) 26 printf ("%d: %dn", i, v[i]); 27 return 0; 28 } F´ıjate en c´omo se indica que el par´ametro a es un vector de enteros: a˜nadiendo un par de corchetes a su identificador. En la l´ınea 23 pasamos a incrementa el vector v. ¿Qu´e ocurre cuando modificamos componentes del par´ametro vectorial a en la l´ınea 10? Si ejecutamos el programa obtenemos el siguiente texto en pantalla: Al principio: 0: 0 1: 1 2: 2 Despu´es de llamar a incrementa: 0: 1 1: 2 2: 3 ¡El contenido de v se ha modificado! Ocurre lo mismo que ocurr´ıa en Python: los vectores s´ı modifican su contenido cuando se altera el contenido del respectivo par´ametro en las llamadas a funci´on. Cuando se pasa un par´ametro vectorial a una funci´on no se efect´ua una copia de su contenido en la pila: s´olo se copia la referencia a la posici´on de memoria en la que empieza el vector. ¿Por qu´e? Por eficiencia: no es infrecuente que los programas manejen vectores de tama˜no considerable; copiarlos cada vez en la pila supondr´ıa invertir una cantidad de tiempo que, para vectores de tama˜no medio o grande, podr´ıa ralentizar dr´asticamente la ejecuci´on del programa. La aproximaci´on adoptada por C hace que s´olo sea necesario copiar en la pila 4 bytes, que es lo que ocupa una direcci´on de memoria. Y no importa cu´an grande o peque˜no sea un vector: la direcci´on de su primer valor siempre ocupa 4 bytes. Veamos gr´aficamente, pues, qu´e ocurre en diferentes instantes de la ejecuci´on del programa. Justo antes de ejecutar la l´ınea 23 tenemos esta disposici´on de elementos en memoria: main 3i v 0 0 1 1 2 2 En el momento de ejecutar la l´ınea 10 por primera vez, en la funci´on incrementa, la memoria presenta este aspecto: 156 Introducci´on a la Programaci´on con C
  • 163. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones main 3i v 0 0 1 1 2 2 llamada desde l´ınea 23 incrementa 0i a ¿Ves? El par´ametro a apunta a v. Los cambios sobre elementos del vector a que tienen lugar al ejecutar la l´ınea 10 tienen efecto sobre los correspondientes elementos de v, as´ı que v refleja los cambios que experimenta a. Tras ejecutar el bucle de incrementa, tenemos esta situaci´on: main 3i v 1 0 2 1 3 2 llamada desde l´ınea 23 incrementa 3i a Y una vez ha finalizado la ejecuci´on de incrementa, ´esta otra: main 3i v 1 0 2 1 3 2 ¿Y qu´e ocurre cuando el vector es una variable global? Pues b´asicamente lo mismo: las refe- rencias no tienen por qu´e ser direcciones de memoria de la pila. Este programa es b´asicamente id´entico al anterior, s´olo que v es ahora una variable global: pasa vector 1.c pasa vector.c 1 #include <stdio.h> 2 3 #define TALLA 3 4 5 int v[TALLA]; 6 7 void incrementa(int a[]) 8 { 9 int i; 10 11 for (i=0; i<TALLA; i++) 12 a[i]++; 13 } 14 15 int main(void) 16 { 17 int i; 18 19 printf ("Al principio:n"); 20 for (i=0; i<TALLA; i++) { Introducci´on a la Programaci´on con C 157
  • 164. 3.5 Paso de par´ametros 21 v[i] = i; 22 printf ("%d: %dn", i, v[i]); 23 } 24 incrementa(v); 25 printf ("Despu´es de llamar a incrementa:n"); 26 for (i=0; i<TALLA; i++) 27 printf ("%d: %dn", i, v[i]); 28 return 0; 29 } Analicemos qu´e ocurre en diferentes instantes de la ejecuci´on del programa. Justo antes de ejecutar la l´ınea 24, existen las variables locales a main y las variables globales: main 3i variables globales v 0 0 1 1 2 2 Al llamar a incrementa se suministra un puntero a la zona de memoria de variables globales, pero no hay problema alguno: el par´ametro a es un puntero que apunta a esa direcci´on. main 3i llamada desde l´ınea 24 incrementa 0i a variables globales v 0 0 1 1 2 2 Los cambios al contenido de a se manifiestan en v: main 3i llamada desde l´ınea 24 incrementa 3i a variables globales v 1 0 2 1 3 2 Y una vez ha finalizado la ejecuci´on de incrementa, el contenido de v queda modificado: main 3i variables globales v 1 0 2 1 3 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 170 Dise˜na un programa C que manipule polinomios de grado menor o igual que 10. Un polinomio se representar´a con un vector de float de tama˜no 11. Si p es un vector que representa un polinomio, p[i] es el coeficiente del t´ermino de grado i. Dise˜na un procedimiento suma con el siguiente perfil: void suma(float p[], float q[], float r[]) El procedimiento modificar´a r para que contenga el resultado de sumar los polinomios p y q. · 171 Dise˜na una funci´on que, dada una cadena y un car´acter, diga cu´antas veces aparece el car´acter en la cadena. 158 Introducci´on a la Programaci´on con C
  • 165. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Hemos visto c´omo pasar vectores a funciones. Has de ser consciente de que no hay forma de saber cu´antos elementos tiene el vector dentro de una funci´on: f´ıjate en que no se indica cu´antos elementos tiene un par´ametro vectorial. Si deseas utilizar el valor de la talla de un vector tienes dos posibilidades: 1. saberlo de antemano, 2. o proporcionarlo como par´ametro adicional. Estudiemos la primera alternativa. F´ıjate en este fragmento de programa: pasa vector talla.c pasa vector talla.c 1 #include <stdio.h> 2 3 #define TALLA1 20 4 #define TALLA2 10 5 6 void inicializa(int z[]) 7 { 8 int i; 9 10 for (i=0; i<TALLA1; i++) 11 z[i] = 0; 12 } 13 14 void imprime(int z[]) 15 { 16 int i; 17 18 for (i=0; i<TALLA1; i++) 19 printf ("%d ", z[i]); 20 printf ("n"); 21 } 22 23 int main(void) 24 { 25 int x[TALLA1]; 26 int y[TALLA2]; 27 28 inicializa(x); 29 inicializa(y); // ! Ojo! 30 31 imprime(x); 32 imprime(y); // ! Ojo! 33 34 return 0; 35 } Siguiendo esta aproximaci´on, la funci´on inicializa s´olo se puede utilizar con vectores de int de talla TALLA1, como x. No puedes llamar a inicializa con y: si lo haces (¡y C te deja hacerlo!) cometer´as un error de acceso a memoria que no te est´a reservada, pues el bucle recorre TALLA1 componentes, aunque y s´olo tenga TALLA2. Ese error puede abortar la ejecuci´on del programa o, peor a´un, no haci´endolo pero alterando la memoria de alg´un modo indefinido. Este es el resultado obtenido en un ordenador concreto: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 El programa no ha abortado su ejecuci´on, pero ha mostrado 20 valores del vector y, que s´olo tiene 10. ¿C´omo podemos dise˜nar una funci´on que pueda trabajar tanto con el vector x como con el vector y? Siguiendo la segunda aproximaci´on propuesta, es decir, pasando como par´ametro adicional la talla del vector en cuesti´on: Introducci´on a la Programaci´on con C 159
  • 166. 3.5 Paso de par´ametros pasa vector talla 1.c pasa vector talla.c 1 #include <stdio.h> 2 3 #define TALLA1 20 4 #define TALLA2 10 5 6 void inicializa(int z[], int talla ) 7 { 8 int i; 9 10 for (i=0; i<talla ; i++) 11 z[i] = 0; 12 } 13 14 void imprime(int z[], int talla ) 15 { 16 int i; 17 18 for (i=0; i<talla ; i++) 19 printf ("%d ", z[i]); 20 printf ("n"); 21 } 22 23 24 int main(void) 25 { 26 int x[TALLA1]; 27 int y[TALLA2]; 28 29 inicializa(x, TALLA1); 30 inicializa(y, TALLA2); 31 32 imprime(x, TALLA1); 33 imprime(y, TALLA2); 34 35 return 0; 36 } Ahora puedes llamar a la funci´on inicializa con inicializa(x, TALLA1) o inicializa(y, TALLA2). Lo mismo ocurre con imprime. El par´ametro talla toma el valor apropiado en cada caso porque t´u se lo est´as pasando expl´ıcitamente. ´Este es el resultado de ejecutar el programa ahora: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 Correcto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 172 Dise˜na un procedimiento ordena que ordene un vector de enteros. El procedimiento recibir´a como par´ametros un vector de enteros y un entero que indique el tama˜no del vector. · 173 Dise˜na una funci´on que devuelva el m´aximo de un vector de enteros. El tama˜no del vector se suministrar´a como par´ametro adicional. · 174 Dise˜na una funci´on que diga si un vector de enteros es o no es pal´ındromo (devolviendo 1 o 0, respectivamente). El tama˜no del vector se suministrar´a como par´ametro adicional. · 175 Dise˜na una funci´on que reciba dos vectores de enteros de id´entica talla y diga si son iguales o no. El tama˜no de los dos vectores se suministrar´a como par´ametro adicional. · 176 Dise˜na un procedimiento que reciba un vector de enteros y muestre todos sus com- ponentes en pantalla. Cada componente se representar´a separado del siguiente con una coma. El ´ultimo elemento ir´a seguido de un salto de l´ınea. La talla del vector se indicar´a con un par´ametro adicional. 160 Introducci´on a la Programaci´on con C
  • 167. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones · 177 Dise˜na un procedimiento que reciba un vector de float y muestre todos sus componentes en pantalla. Cada componente se representar´a separado del siguiente con una coma. Cada 6 componentes aparecer´a un salto de l´ınea. La talla del vector se indicar´a con un par´ametro adicional. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.5. Par´ametros escalares: paso por referencia mediante punteros C permite modificar el valor de variables escalares en una funci´on recurriendo a sus direcciones de memoria. Analicemos el siguiente ejemplo: referencia local.c referencia local.c 1 #include <stdio.h> 2 3 void incrementa(int * a) 4 { 5 *a += 1; 6 } 7 8 int main(void) 9 { 10 int b; 11 12 b = 1; 13 printf ("Al principio b vale %dn", b); 14 incrementa(&b); 15 printf ("Y al final vale %dn", b); 16 return 0; 17 } Al ejecutarlo, aparece en pantalla el siguiente texto: Al principio b vale 1 Y al final vale 2 Efectivamente, b ha modificado su valor tras la llamada a incrementa. Observa la forma en que se ha declarado el ´unico par´ametro de incrementa: int * a. O sea, a es del tipo int *. Un tipo de la forma ((tipo *)) significa ((puntero a valor de tipo tipo)). Tenemos, por tanto, que a es un ((puntero a entero)). No le pasamos a la funci´on el valor de un entero, sino el valor de la direcci´on de memoria en la que se encuentra un entero. F´ıjate ahora en c´omo pasamos el argumento en la llamada a incrementa de la l´ınea 14, que es de la forma incrementa(&b). Estamos pasando la direcci´on de memoria de b (que es lo que proporciona el operador &) y no el valor de b. Todo correcto, ya que hemos dicho que la funci´on espera la direcci´on de memoria de un entero. Al principio de la ejecuci´on de incrementa tendremos esta situaci´on: main 1b llamada desde l´ınea 14 incrementa a El par´ametro a es un puntero que apunta a b. F´ıjate ahora en la sentencia que incrementa el valor apuntado por a (l´ınea 5): *a += 1; El asterisco que precede a a no indica ((multiplicaci´on)). Ese asterisco es un operador unario que hace justo lo contrario que el operador &: dada una direcci´on de memoria, accede al valor de la variable apuntada. (Recuerda que el operador & obten´ıa la direcci´on de memoria de una Introducci´on a la Programaci´on con C 161
  • 168. 3.5 Paso de par´ametros El & de los par´ametros de scanf Ahora ya puedes entender bien por qu´e las variables escalares que suministramos a scanf para leer su valor por teclado van precedidas por el operador &: como scanf debe modificar su valor, ha de saber en qu´e direcci´on de memoria residen. No ocurre lo mismo cuando vamos a leer una cadena, pero eso es porque el identificador de la variable ya es, en ese caso, una direcci´on de memoria. variable.) O sea, C interpreta *a como accede a la variable apuntada por a, que es b, as´ı que *a += 1 equivale a b += 1 e incrementa el contenido de la variable b. ¿Qu´e pasar´ıa si en lugar de *a += 1 hubi´esemos escrito a += 1? Se hubiera incrementado la direcci´on de memoria a la que apunta el puntero, nada m´as. ¿Y si hubi´esemos escrito a++? Lo mismo: hubi´esemos incrementado el valor de la direcci´on almacenada en a. ¿Y *a++?, ¿funcionar´ıa? A primera vista dir´ıamos que s´ı, pero no funciona como esperamos. El operador ++ postfijo tiene mayor nivel de precedencia que el operador unario *, as´ı que *a++ (post)incrementa la direcci´on a y accede a su contenido, por ese ´orden. Nuevamente habr´ıamos incrementado el valor de la direcci´on de memoria, y no su contenido. Si quieres usar operadores de incremento/decremento, tendr´as que utilizar par´entesis para que los operadores se apliquen en el orden deseado: (*a)++. Naturalmente, no s´olo puedes acceder as´ı a variables locales, tambi´en las variables globales son accesibles mediante punteros: referencia global.c referencia global.c 1 #include <stdio.h> 2 3 int b; // Variable global. 4 5 void incrementa(int * a) 6 { 7 *a += 1; 8 } 9 10 int main(void) 11 { 12 b = 1; 13 printf ("Al principio b vale %dn", b); 14 incrementa(&b); 15 printf ("Y al final vale %dn", b); 16 return 0; 17 } El aspecto de la memoria cuando empieza a ejecutarse la funci´on incrementa es ´este: main llamada desde l´ınea 14 incrementa a variables globales 1b . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 178 Dise˜na un procedimiento que modifique el valor del par´ametro de tipo float para que valga la inversa de su valor cuando ´este sea distinto de cero. Si el n´umero es cero, el procedimiento dejar´a intacto el valor del par´ametro. Si a vale 2.0, por ejemplo, inversa(&a) har´a que a valga 0.5. · 179 Dise˜na un procedimiento que intercambie el valor de dos n´umeros enteros. Si a y b valen 1 y 2, respectivamente, la llamada intercambia(&a, &b) har´a que a pase a valer 2 y b pase a valer 1. · 180 Dise˜na un procedimiento que intercambie el valor de dos n´umeros float. 162 Introducci´on a la Programaci´on con C
  • 169. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones La dualidad vector/puntero, el paso de vectores y el paso por referencia Cuando pasamos un vector a una funci´on estamos pasando, realmente, una direcci´on de memoria: aquella en la que empieza la zona de memoria reservada para el vector. Cuando pasamos una variable escalar por referencia, tambi´en estamos pasando una direcci´on de memoria: aquella en la que empieza la zona de memoria reservada para el valor escalar. ¿Qu´e diferencia hay entre una y otra direcci´on? Ninguna: un puntero siempre es un puntero. F´ıjate en este programa: dualidad.c dualidad.c 1 #include <stdio.h> 2 3 #define TALLA 10 4 5 void procedimiento(int *a, int b[]) 6 { 7 printf ("%22d %6dn", *a, b[0]); 8 printf ("%22d %6dn", a[0], *b); // ! Ojo! 9 } 10 11 int main(void) 12 { 13 int x[TALLA], i, y = 10; 14 15 for (i=0; i<TALLA; i++) x[i] = i + 1; 16 printf ("1) procedimiento( &y, x):n"); 17 procedimiento(&y, x); 18 printf ("2) procedimiento( x, &y):n"); 19 procedimiento(x, &y); 20 printf ("3) procedimiento(&x[0], &x[1]):n"); 21 procedimiento(&x[0], &x[1]); 22 23 return 0; 24 } Esta es la salida resultante de su ejecuci´on: 1) procedimiento( &y, x): 10 1 10 1 2) procedimiento( x, &y): 1 10 1 10 3) procedimiento(&x[0], &x[1]): 1 2 1 2 Observa qu´e ha ocurrido: en procedimiento se puede usar a y b como si fueran vectores o variables escalares pasadas por referencia. Y podemos pasar a procedimiento tanto la direcci´on de un vector de ints como la direcci´on de una variable escalar de tipo int. La conclusi´on es clara: ((int * a)) e ((int b[])) son sin´onimos cuando se declara un par´ametro, pues en ambos casos se describen punteros a direcciones de memoria en las que residen sendos valores enteros (o donde empieza una serie de valores enteros). Aunque sean expresiones sin´onimas y, por tanto, intercambiables, interesa que las uses ((correctamente)), pues as´ı mejorar´a la legibilidad de tus programas: usa int * cuando quieras pasar la direcci´on de un entero y int [] cuando quieras pasar la direcci´on de un vector de enteros. · 181 Dise˜na un procedimiento que asigne a todos los elementos de un vector de enteros un valor determinado. El procedimiento recibir´a tres datos: el vector, su n´umero de elementos y el valor que que asignamos a todos los elementos del vector. · 182 Dise˜na un procedimiento que intercambie el contenido completo de dos vectores de enteros de igual talla. La talla se debe suministrar como par´ametro. Introducci´on a la Programaci´on con C 163
  • 170. 3.5 Paso de par´ametros En C s´olo hay paso por valor Este apartado intenta que aprendas a distinguir el paso de par´ametros por valor y por referencia. ¡Pero la realidad es que C s´olo tiene paso de par´ametros por valor! Cuando pasas una referencia, est´as pasando expl´ıcitamente una direcci´on de memoria gracias al operador &, y lo que hace C es copiar dicha direcci´on en la pila, es decir, pasa por valor una direcci´on para simular el paso de par´ametros por referencia. La extra˜na forma de pasar el par´ametro hace que tengas que usar el operador * cada vez que deseas acceder a ´el en el cuerpo de la funci´on. En otros lenguajes, como Pascal, es posible indicar que un par´ametro se pasa por referen- cia sin que tengas que usar un operador (equivalente a) & al efectuar el paso o un operador (equivalente a) * cuando usas el par´ametro en el cuerpo de la funci´on. Por ejemplo, este programa Pascal incluye un procedimiento que modifica el valor de su par´ametro: program referencia; var b : integer; procedure incrementa (var a : integer); begin a := a + 1; end; begin (* programa principal *) b := 1; writeln(’b val´ıa ’, b); incrementa(b); writeln(’b vale ’, b) end. C++ es una extensi´on de C que permite el paso de par´ametros por referencia. Usa para ello el car´acter & en la declaraci´on del par´ametro: 1 #include <stdio.h> 2 3 void incrementa(int & a) 4 { 5 a += 1; 6 } 7 8 int main(void) 9 { 10 int b; 11 12 b = 1; 13 printf ("Al principio b vale %dn", b); 14 incrementa(b); 15 printf ("Y al final vale %dn", b); 16 return 0; 17 } (Aunque no venga a cuento, observa lo diferente que es C de Pascal (y aun as´ı, lo semejante que es) y c´omo el programa C++ presenta un aspecto muy semejante a uno equivalente escrito en C.) · 183 Dise˜na un procedimiento que asigne a un entero la suma de los elementos de un vector de enteros. Tanto el entero (su direcci´on) como el vector se suministrar´an como par´ametros. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Un uso habitual del paso de par´ametros por referencia es la devoluci´on de m´as de un valor como resultado de la ejecuci´on de una funci´on. Ve´amoslo con un ejemplo. Dise˜nemos una funci´on que, dados un ´angulo α (en radianes) y un radio r, calcule el valor de x = r cos(α) e y = r sin(α): 164 Introducci´on a la Programaci´on con C
  • 171. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones x yr α No podemos dise˜nar una funci´on que devuelva los dos valores. Hemos de dise˜nar un procedi- miento que devuelva los valores resultantes como par´ametros pasados por referencia: paso por referencia.c 1 #include <stdio.h> 2 #include <math.h> 3 4 void calcula_xy(float alfa, float radio, float * x, float * y) 5 { 6 *x = radio * cos(alfa); 7 *y = radio * sin(alfa); 8 } ¿Y c´omo llamamos al procedimiento? Aqu´ı tienes un ejemplo de uso: paso por referencia.c paso por referencia.c . . . 8 } 9 10 int main(void) 11 { 12 float r, angulo, horizontal, vertical; 13 14 printf ("Introduce el ´angulo (en radianes): "); scanf ("%f", &angulo); 15 printf ("Introduce el radio: "); scanf ("%f", &r); 16 calcula_xy(angulo, r, &horizontal , &vertical ); 17 printf ("Resultado: (%f, %f)n", horizontal , vertical ); 18 19 return 0; 20 } ¿Ves? Las variables horizontal y vertical no se inicializan en main: reciben valores como resul- tado de la llamada a calcula_xy. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 184 Dise˜na una funci´on que calcule la inversa de calcula_xy, es decir, que obtenga el valor del radio y del ´angulo a partir de x e y. · 185 Dise˜na una funci´on que reciba dos n´umeros enteros a y b y devuelva, simult´aneamente, el menor y el mayor de ambos. La funci´on tendr´a esta cabecera: 1 void minimax (int a, int b, int * min, int * max) · 186 Dise˜na una funci´on que reciba un vector de enteros, su talla y un valor de tipo entero al que denominamos buscado. La funci´on devolver´a (mediante return) el valor 1 si buscado tiene el mismo valor que alg´un elemento del vector y 0 en caso contrario. La funci´on devolver´a, adem´as, la distancia entre buscado y el elemento m´as pr´oximo a ´el. La cabecera de la funci´on ha de ser similar a ´esta: 1 int busca(int vector[], int talla, int buscado, int * distancia) Te ponemos un par de ejemplos para que veas qu´e debe hacer la funci´on. 1 #include <stdio.h> 2 3 #define TALLA 6 Introducci´on a la Programaci´on con C 165
  • 172. 3.5 Paso de par´ametros 4 5 // Define aqu´ı la funci´on 6 ... 7 8 int main(void) 9 { 10 int v[TALLA], distancia, encontrado, buscado, i; 11 12 for (i=0; i<TALLA; i++) { 13 printf ("Introduce el elemento %d: ", i); 14 scanf ("%d", &v[i]); 15 } 16 17 printf (" ? Qu´e valor busco?: "); 18 scanf ("%d", &buscado); 19 20 encontrado = busca(v, TALLA, buscado, &distancia); 21 if (encontrado) 22 printf ("Encontr´e el valor %d.n", buscado); 23 else 24 printf ("No est´a. El elemento m´as pr´oximo est´a a distancia %d.n", distancia); 25 26 printf (" ? Qu´e valor busco ahora?: "); 27 scanf ("%d", &buscado); 28 29 encontrado = busca(v, TALLA, buscado, &distancia); 30 if (encontrado) 31 printf ("Encontr´e el valor %d.n", buscado); 32 else 33 printf ("No est´a. El elemento m´as pr´oximo est´a a distancia %d.n", distancia); 34 35 return 0; 36 } Al ejecutar el programa obtenemos esta salida por pantalla: Introduce el elemento: 0 Introduce el elemento: 5 Introduce el elemento: 10 Introduce el elemento: 15 Introduce el elemento: 20 Introduce el elemento: 25 ? Qu´e valor busco?: 5 Encontr´e el valor 5. ? Qu´e valor busco ahora?: 17 No est´a. El elemento m´as pr´oximo est´a a distancia 2. · 187 Modifica la funci´on del ejercicio anterior para que, adem´as de la distancia al elemento m´as pr´oximo, devuelva el valor del elemento m´as pr´oximo. · 188 Modifica la funci´on del ejercicio anterior para que, adem´as de la distancia al elemento m´as pr´oximo y el elemento m´as pr´oximo, devuelva el valor de su ´ındice. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.6. Paso de registros a funciones No s´olo puedes pasar escalares y vectores como argumentos, tambi´en puedes pasar registros. El paso de registros es por valor, o sea, copiando el contenido en la pila, a menos que t´u mismo pases un puntero a su direcci´on de memoria. Este programa, por ejemplo, define un tipo de datos para representar puntos en un espacio de tres dimensiones y una funci´on que calcula la distancia de un punto al origen: 166 Introducci´on a la Programaci´on con C
  • 173. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones 1 #include <stdio.h> 2 #include <math.h> 3 4 struct Punto { 5 float x, y, z; 6 }; 7 8 float distancia(struct Punto p) 9 { 10 return sqrt( p.x*p.x + p.y*p.y + p.z*p.z ); 11 } 12 13 int main(void) 14 { 15 struct Punto pto; 16 17 pto.x = 1; 18 pto.y = 1; 19 pto.z = 1; 20 21 printf ("Distancia al origen: %fn", distancia(pto)); 22 return 0; 23 } Al pasar un registro a la funci´on, C copia en la pila cada uno de los valores de sus campos. Ten en cuenta que una variable de tipo struct Punto ocupa 24 bytes (contiene 3 valores de tipo float). Variables de otros tipos registro que definas pueden ocupar cientos o incluso miles de bytes, as´ı que ve con cuidado: llamar a una funci´on pasando registros por valor puede resultar ineficiente. Por cierto, no es tan extra˜no que un registro ocupe cientos de bytes: uno o m´as de sus campos podr´ıa ser un vector. Tambi´en en ese caso se estar´ıa copiando su contenido ´ıntegro en la pila. Eso s´ı, como est´as pasando una copia, las modificaciones del valor de un campo en el cuerpo de la funci´on no tendr´an efectos perceptibles fuera de la funci´on. Como te hemos anticipado, tambi´en puedes pasar registros por referencia. En tal caso s´olo se estar´a copiando en la pila la direcci´on de memoria en la que empieza el registro (y eso son 4 bytes), mida lo que mida ´este. Se trata, pues, de un paso de par´ametros m´as eficiente. Eso s´ı, has de tener en cuenta que los cambios que efect´ues a cualquier campo del par´ametro se reflejar´an en el campo correspondiente de la variable que suministraste como argumento. Esta funci´on, por ejemplo, define dos par´ametros: uno que se pasa por referencia y otro que se pasa por valor. La funci´on traslada un punto p en el espacio (modificando los campos del punto original) de acuerdo con el vector de desplazamiento que se indica con otro punto (traslacion): 1 void traslada(struct Punto * p, struct Punto traslacion) 2 { 3 (*p).x += traslacion.x; 4 (*p).y += traslacion.y; 5 (*p).z += traslacion.z; 6 } Observa c´omo hemos accedido a los campos de p. Ahora p es una direcci´on de memoria (es de tipo struct Punto *), y *p es la variable apuntada por p (y por tanto, es de tipo struct Punto). El campo x es accedido con (*p).x: primero se accede al contenido de la direcci´on de memoria apuntada por p, y luego al campo x del registro *p, de ah´ı que usemos par´entesis. Es tan frecuente la notaci´on (*p).x que existe una forma compacta equivalente: 1 void traslada(struct Punto * p, struct Punto traslacion) 2 { 3 p->x += traslacion.x; 4 p->y += traslacion.y; 5 p->z += traslacion.z; 6 } La forma p->x es absolutamente equivalente a (*p).x. Introducci´on a la Programaci´on con C 167
  • 174. 3.5 Paso de par´ametros Recuerda, pues, que dentro de una funci´on se accede a los campos de forma distinta seg´un se pase un valor por copia o por referencia: 1. con el operador punto, como en traslacion.x, si la variable se ha pasado por valor; 2. con el operador ((flecha)), como en p->x, si la variable se ha pasado por referencia (equi- valentemente, puedes usar la notaci´on (*p).x). Acabemos este apartado mostrando una rutina que pide al usuario que introduzca las coor- denadas de un punto: 1 void lee_punto(struct Punto * p) 2 { 3 printf ("x: "); scanf ("%f", &p->x); 4 printf ("y: "); scanf ("%f", &p->y); 5 printf ("z: "); scanf ("%f", &p->z); 6 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 189 Este ejercicio y los siguientes de este bloque tienen por objeto construir una serie de funciones que permitan efectuar transformaciones afines sobre puntos en el plano. Los puntos ser´an variables de tipo struct Punto, que definimos as´ı: 1 struct Punto { 2 float x, y; 3 }; Dise˜na un procedimiento muestra_punto que muestre por pantalla un punto. Un punto p tal que p.x vale 2.0 y p.y vale 0.2 se mostrar´a en pantalla as´ı: (2.000000, 0.200000). El procedimiento muestra_punto recibir´a un punto por valor. Dise˜na a continuaci´on un procedimiento que permita leer por teclado un punto. El procedi- miento recibir´a por referencia el punto en el que se almacenar´an los valores le´ıdos. · 190 La operaci´on de traslaci´on permite desplazar un punto de coordenadas (x, y) a (x+a, y+ b), siendo el desplazamiento (a, b) un vector (que representamos con otro punto). Implementa una funci´on que reciba dos par´ametros de tipo punto y modifique el primero de modo que se traslade lo que indique el vector. · 191 La operaci´on de escalado transforma un punto (x, y) en otro (ax, ay), donde a es un factor de escala (real). Implementa una funci´on que escale un punto de acuerdo con el factor de escala a que se suministre como par´ametro (un float). · 192 Si rotamos un punto (x, y) una cantidad de θ radianes alrededor del origen, obtenemos el punto (x cos θ − y sin θ, x sin θ + y cos θ). Define una funci´on que rote un punto la cantidad de grados que se especifique. · 193 La rotaci´on de un punto (x, y) una cantidad de θ radianes alrededor de un punto (a, b) se puede efectuar con una traslaci´on con el vector (−a, −b), una rotaci´on de θ radianes con respecto al origen y una nueva traslaci´on con el vector (a, b). Dise˜na una funci´on que permita trasladar un punto un n´umero dado de grados alrededor de otro punto. · 194 Dise˜na una funci´on que diga si dos puntos son iguales. · 195 Hemos definido un tipo registro para representar complejos as´ı: 1 struct Complejo { 2 float real; 3 float imag; 4 }; Dise˜na e implementa los siguientes procedimientos para su manipulaci´on: leer un complejo de teclado; mostrar un complejo por pantalla; 168 Introducci´on a la Programaci´on con C
  • 175. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones el m´odulo de un complejo (|a + bi| = √ a2 + b2); el opuesto de un complejo (−(a + bi) = −a − bi); el conjugado de un complejo (a + bi = a − bi); la suma de dos complejos ((a + bi) + (c + di) = (a + c) + (b + d)i); la diferencia de dos complejos ((a + bi) − (c + di) = (a − c) + (b − d)i); el producto de dos complejos ((a + bi) · (c + di) = (ac − bd) + (ad + bc)i); la divisi´on de dos complejos (a+bi c+di = ac+bd c2+d2 + bc−ad c2+d2 i). · 196 Define un tipo registro y una serie de funciones para representar y manipular fechas. Una fecha consta de un d´ıa, un mes y un a˜no. Debes implementar funciones que permitan: mostrar una fecha por pantalla con formato dd/mm/aaaa (por ejemplo, el 7 de junio de 2001 se muestra as´ı: 07/06/2001); mostrar una fecha por pantalla como texto (por ejemplo, el 7 de junio de 2001 se muestra as´ı: 7 de junio de 2001); leer una fecha por teclado; averiguar si una fecha cae en a˜no bisiesto; averiguar si una fecha es anterior, igual o posterior a otra, devolviendo los valores −1, 0 o 1 respectivamente, comprobar si una fecha existe (por ejemplo, el 29 de febrero de 2002 no existe): calcular la diferencia de d´ıas entre dos fechas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.7. Paso de matrices y otros vectores multidimensionales El paso de vectores multidimensionales no es una simple extensi´on del paso de vectores unidi- mensionales. Veamos. Aqu´ı tienes un programa incorrecto en el que se define una funci´on que recibe una matriz y devuelve su elemento m´aximo: pasa matriz mal.c E pasa matriz mal.c E 1 #include <stdio.h> 2 3 #define TALLA 3 4 5 int maximo(int a[][]) 6 { 7 int i, j, m; 8 9 m = a[0][0]; 10 for (i=0; i<TALLA; i++) 11 for (j=0; j<TALLA; j++) 12 if (a[i][j] > m) 13 m = a[i][j]; 14 15 return m; 16 } 17 18 int main(void) 19 { 20 int matriz[TALLA][TALLA]; 21 int i, j; 22 23 for (i=0; i<TALLA; i++) Introducci´on a la Programaci´on con C 169
  • 176. 3.5 Paso de par´ametros 24 for (j=0; j<TALLA; j++) 25 matriz[i][j] = (i*j) % TALLA; 26 27 printf ("El m´aximo es %dn", maximo(matriz)); 28 return 0; 29 } El compilador no acepta ese programa. ¿Por qu´e? F´ıjate en la declaraci´on del par´ametro. ¿Qu´e hay de malo? C no puede resolver los accesos de la forma a[i][j]. Si recuerdas, a[i][j] significa ((accede a la celda cuya direcci´on se obtiene sumando a la direcci´on a el valor i * COLUMNAS + j)), donde COLUMNAS es el n´umero de columnas de la matriz a (en nuestro caso, ser´ıa TALLA). Pero, ¿c´omo sabe la funci´on cu´antas columnas tiene a? ¡No hay forma de saberlo viendo una definici´on del estilo int a[][]! La versi´on correcta del programa debe indicar expl´ıcitamente cu´antas columnas tiene la matriz. Hela aqu´ı: pasa matriz.c pasa matriz.c 1 #include <stdio.h> 2 3 #define TALLA 3 4 5 int maximo(int a[][TALLA]) 6 { 7 int i, j, m; 8 9 m = a[0][0]; 10 for (i=0; i<TALLA; i++) 11 for (j=0; j<TALLA; j++) 12 if (a[i][j] > m) 13 m = a[i][j]; 14 15 return m; 16 } 17 18 int main(void) 19 { 20 int matriz[TALLA][TALLA]; 21 int i, j; 22 23 for (i=0; i<TALLA; i++) 24 for (j=0; j<TALLA; j++) 25 matriz[i][j] = (i*j) % TALLA; 26 27 printf ("El m´aximo es %dn", maximo(matriz)); 28 return 0; 29 } No ha sido necesario indicar cu´antas filas tiene la matriz (aunque somos libres de hacerlo). La raz´on es sencilla: el n´umero de filas no hace falta para calcular la direcci´on en la que reside el valor a[i][j]. As´ı pues, en general, es necesario indicar expl´ıcitamente el tama˜no de cada una de las dimensiones del vector, excepto el de la primera (que puedes declarar o no, a voluntad). S´olo as´ı obtiene C informaci´on suficiente para calcular los accesos a elementos del vector en el cuerpo de la funci´on. Una consecuencia de esta restricci´on es que no podremos definir funciones capaces de tra- bajar con matrices de tama˜no arbitrario. Siempre hemos de definir expl´ıcitamente el tama˜no de cada dimensi´on excepto de la primera. Habr´a una forma de superar este inconveniente, pero tendremos que esperar al siguiente cap´ıtulo para poder estudiarla. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 197 Vamos a dise˜nar un programa capaz de jugar al tres en raya. El tablero se representar´a con una matriz de 3 × 3. Las casillas ser´an caracteres. El espacio en blanco representar´a una casilla vac´ıa; el car´acter ’o’ representar´a una casilla ocupada con un c´ırculo y el car´acter ’x’ representar´a una casilla marcada con una cruz. 170 Introducci´on a la Programaci´on con C
  • 177. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones Dise˜na una funci´on que muestre por pantalla el tablero. Dise˜na una funci´on que detecte si el tablero est´a lleno. Dise˜na una funci´on que detecte si alg´un jugador consigui´o hacer tres en raya. Dise˜na una funci´on que solicite al usuario la jugada de los c´ırculos y modifique el tablero adecuadamente. La funci´on debe detectar si la jugada es v´alida o no. Dise˜na una funci´on que, dado un tablero, realice la jugada que corresponde a las cruces. En una primera versi´on, haz que el ordenador ponga la cruz en la primera casilla libre. Despu´es, modifica la funci´on para que el ordenador realice la jugada m´as inteligente. Cuando hayas dise˜nado todas las funciones, monta un programa que las use y permita jugar al tres en raya contra el computador. · 198 El juego de la vida se juega sobre una matriz cuyas celdas pueden estar vivas o muertas. La matriz se modifica a partir de su estado siguiendo unas sencilla reglas que tienen en cuenta los, como mucho, 8 vecinos de cada casilla: Si una celda viva est´a rodeada por 0 o 1 celdas vivas, muere de soledad. Si una celda viva est´a rodeada por 4 celdas vivas, muere por superpoblaci´on. Si una celda viva est´a rodeada por 2 o 3 celdas vivas, sigue viva. Una celda muerta s´olo resucita si est´a rodeada por 3 celdas vivas. Dise˜na una funci´on que reciba una matriz de 10 × 10 celdas en la que el valor 0 representa ((celda muerta)) y el valor 1 representa ((celda viva)). La funci´on modificar´a la matriz de acuerdo con las reglas del juego de la vida. (Avisos: Necesitar´as una matriz auxiliar. Las celdas de los bordes no tienen 8 vecinos, sino 3 o 5.) A continuaci´on, monta un programa que permita al usuario introducir una disposici´on inicial de celdas y ejecutar el juego de la vida durante n ciclos, siendo n un valor introducido por el usuario. Aqu´ı tienes un ejemplo de ((partida)) de 3 ciclos con una configuraci´on inicial curiosa: Configuraci´on inicial: __________ ______xxx_ __________ __________ ___xxx____ __xxx_____ __________ __________ __________ __________ Ciclos: 3 _______x__ _______x__ _______x__ ____x_____ __x__x____ __x__x____ ___x______ __________ __________ __________ __________ ______xxx_ __________ __________ ___xxx____ __xxx_____ Introducci´on a la Programaci´on con C 171
  • 178. 3.5 Paso de par´ametros __________ __________ __________ __________ _______x__ _______x__ _______x__ ____x_____ __x__x____ __x__x____ ___x______ __________ __________ __________ · 199 Implementa el juego del buscaminas. El juego del buscaminas se juega en un tablero de dimensiones dadas. Cada casilla del tablero puede contener una bomba o estar vac´ıa. Las bombas se ubican aleatoriamente. El usuario debe descubrir todas las casillas que no contienen bomba. Con cada jugada, el usuario descubre una casilla (a partir de sus coordenadas, un par de letras). Si la casilla contiene una bomba, la partida finaliza con la derrota del usuario. Si la casilla est´a libre, el usuario es informado de cu´antas bombas hay en las (como mucho) 8 casillas vecinas. Este tablero representa, en un terminal, el estado actual de una partida sobre un tablero de 8 × 8: abcdefgh a 00001___ b 00112___ c 222_____ d ________ e ____3___ f ________ g 1_111111 h __100000 Las casillas con un punto no han sido descubiertas a´un. Las casillas con un n´umero han sido descubiertas y sus casillas vecinas contienen tantas bombas como se indica en el n´umero. Por ejemplo, la casilla de coordenadas (’e’, ’e’) tiene 3 bombas en la vecindad y la casilla de coordenadas (’b’, ’a’), ninguna. Implementa un programa que permita seleccionar el nivel de dificultad y, una vez escogido, genere un tablero y permita jugar con ´el al jugador. Los niveles de dificultad son: f´acil: tablero de 8 × 8 con 10 bombas. medio: tablero de 15 × 15 con 40 bombas. dif´ıcil: tablero de 20 × 20 con 100 bombas. Debes dise˜nar funciones para desempe˜nar cada una de las acciones b´asicas de una partida: dado un tablero y las coordenadas de una casilla, indicar si contiene bomba o no, dado un tablero y las coordenadas de una casilla, devolver el n´umero de bombas vecinas, dado un tablero y las coordenadas de una casilla, modificar el tablero para indicar que la casilla en cuesti´on ya ha sido descubierta, dado un tablero, mostrar su contenido en pantalla, etc. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172 Introducci´on a la Programaci´on con C
  • 179. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones 3.5.8. Tipos de retorno v´alidos Una funci´on puede devolver valores de cualquier tipo escalar o de registros, pero no puede devolver vectores3 . La raz´on es simple: la asignaci´on funciona con valores escalares y registros, pero no con vectores. Ya hemos visto c´omo devolver valores escalares. A t´ıtulo ilustrativo te presentamos un ejemplo de definici´on de registro y definici´on de funci´on que recibe como par´ametros un punto (x, y) y un n´umero y devuelve un nuevo punto cuyo valor es (ax, ay): 1 struct Punto { 2 float x, y; 3 }; 4 5 struct Punto escala(struct Punto p, float a) 6 { 7 struct Punto q; 8 9 q.x = a * p.x; 10 q.y = a * p.y; 11 12 return q; 13 } Eso es todo. . . por el momento. Volveremos a la cuesti´on de si es posible devolver vectores cuando estudiemos la gesti´on de memoria din´amica. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 200 Vuelve a implementar las funciones de manipulaci´on de puntos en el plano (ejerci- cios 189–194) para que no modifiquen el valor del registro struct Punto que se suministra como par´ametro. En su lugar, devolver´an el punto resultante como valor de retorno de la llamada a funci´on. · 201 Implementa nuevamente las funciones del ejercicio 195, pero devolviendo un nuevo complejo con el resultado de operar con el complejo o complejos que se suministran como par´ametros. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.9. Un ejercicio pr´actico: miniGalaxis Pongamos en pr´actica lo aprendido dise˜nando una versi´on simplificada de un juego de rescate espacial (Galaxis)4 al que denominaremos miniGalaxis. MiniGalaxis se juega con un tablero de 9 filas y 20 columnas. En el tablero hay 5 n´aufragos espaciales y nuestro objetivo es descubrir d´onde se encuentran. Contamos para ello con una sonda espacial que podemos activar en cualquier casilla del tablero. La sonda dispara una se˜nal en las cuatro direcciones cardinales que es devuelta por unos dispositivos que llevan los n´aufragos. La sonda nos dice cu´antos n´aufragos espaciales han respondido, pero no desde qu´e direcciones enviaron su se˜nal de respuesta. Cuando activamos la sonda en las coordenadas exactas en las que se encuentra un na´ufrago, lo damos por rescatado. S´olo disponemos de 20 sondas para efectuar el rescate, as´ı que las hemos de emplear juiciosamente. De lo contrario, la muerte de inocentes pesar´a sobre nuestra conciencia. Lo mejor ser´a que te hagas una idea precisa del juego jugando. Al arrancar aparece esta informaci´on en pantalla: ABCDEFGHIJKLMNOPQRST 0 ++++++++++++++++++++ 1 ++++++++++++++++++++ 2 ++++++++++++++++++++ 3 ++++++++++++++++++++ 4 ++++++++++++++++++++ 3Al menos no hasta que sepamos m´as de la gesti´on de memoria din´amica 4El nombre y la descripci´on puede que te hagan concebir demasiadas esperanzas: se trata de un juego muy sencillito y falto de cualquier efecto especial. Galaxis fue concebido por Christian Franz y escrito para el Apple Macintosh. M´as tarde, Eric Raymond lo reescribi´o para que fuera ejecutable en Unix. Introducci´on a la Programaci´on con C 173
  • 180. 3.5 Paso de par´ametros 5 ++++++++++++++++++++ 6 ++++++++++++++++++++ 7 ++++++++++++++++++++ 8 ++++++++++++++++++++ Hay 5 n´aufragos. Dispones de 20 sondas. Coordenadas: El tablero se muestra como una serie de casillas. Arriba tienes letras para identificar las columnas y a la izquierda n´umeros para las filas. El ordenador nos informa de que a´un quedan 5 n´aufragos por rescatar y que disponemos de 20 sondas. Se ha detenido mostrando el mensaje ((Coordenadas:)): est´a esperando a que digamos en qu´e coordenadas lanzamos una sonda. El ordenador acepta una cadena que contenga un d´ıgito y una letra (en cualquier orden) y la letra puede ser min´uscula o may´uscula. Lancemos nuestra primera sonda: escribamos 5b y pulsemos la tecla de retorno de carro. He aqu´ı el resultado: Coordenadas: 5b ABCDEFGHIJKLMNOPQRST 0 +.++++++++++++++++++ 1 +.++++++++++++++++++ 2 +.++++++++++++++++++ 3 +.++++++++++++++++++ 4 +.++++++++++++++++++ 5 .0.................. 6 +.++++++++++++++++++ 7 +.++++++++++++++++++ 8 +.++++++++++++++++++ Hay 5 n´aufragos. Dispones de 19 sondas. Coordenadas: El tablero se ha redibujado y muestra el resultado de lanzar la sonda. En la casilla de coordenadas 5b aparece un cero: es el n´umero de na´ufragos que hemos detectado con la sonda. Mala suerte. Las casillas que ahora aparecen con un punto son las exploradas por la sonda. Ahora sabes que en ninguna de ellas hay un n´aufrago. Sigamos jugando: probemos con las coordenadas 3I. Aqu´ı tienes la respuesta del ordenador: Coordenadas: 3I ABCDEFGHIJKLMNOPQRST 0 +.++++++.+++++++++++ 1 +.++++++.+++++++++++ 2 +.++++++.+++++++++++ 3 ........1........... 4 +.++++++.+++++++++++ 5 .0.................. 6 +.++++++.+++++++++++ 7 +.++++++.+++++++++++ 8 +.++++++.+++++++++++ Hay 5 n´aufragos. Dispones de 18 sondas. Coordenadas: En la casilla de coordenadas 3I aparece un uno: la sonda ha detectado la presencia de un n´aufrago en alguna de las 4 direcciones. Sigamos. Probemos en 0I: Coordenadas: i0 ABCDEFGHIJKLMNOPQRST 0 ........2........... 1 +.++++++.+++++++++++ 2 +.++++++.+++++++++++ 3 ........1........... 4 +.++++++.+++++++++++ 5 .0.................. 6 +.++++++.+++++++++++ 174 Introducci´on a la Programaci´on con C
  • 181. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones 7 +.++++++.+++++++++++ 8 +.++++++.+++++++++++ Hay 5 n´aufragos. Dispones de 17 sondas. Coordenadas: Dos n´aufragos detectados. Parece probable que uno de ellos est´e en la columna I. Lancemos otra sonda en esa columna. Probemos con 2I: Coordenadas: 2I ABCDEFGHIJKLMNOPQRST 0 ........2........... 1 +.++++++.+++++++++++ 2 ........X........... 3 ........1........... 4 +.++++++.+++++++++++ 5 .0.................. 6 +.++++++.+++++++++++ 7 +.++++++.+++++++++++ 8 +.++++++.+++++++++++ Hay 4 n´aufragos. Dispones de 16 sondas. Coordenadas: ¡Bravo! Hemos encontrado a uno de los n´aufragos. En el tablero se muestra con una X. Ya s´olo quedan 4. Bueno. Con esta partida inacabada puedes hacerte una idea detallada del juego. Dise˜nemos el programa. Empezamos por definir las estructuras de datos. La primera de ellas, el tablero de juego, que es una simple matriz de 9 × 20 casillas. Nos vendr´a bien disponer de constantes que almacenen el n´umero de filas y columnas para usarlas en la definici´on de la matriz: 1 #include <stdio.h> 2 3 #define FILAS 9 4 #define COLUMNAS 20 5 6 int main(void) 7 { 8 char espacio[FILAS][COLUMNAS]; 9 10 return 0; 11 } La matriz espacio es una matriz de caracteres. Hemos de inicializarla con caracteres ’+’, que indican que no se han explorado sus casillas. En lugar de inicializarla en main, vamos a dise˜nar una funci´on especial para ello. ¿Por qu´e? Para mantener main razonablemente peque˜no y mejorar as´ı la legibilidad. A estas alturas no debe asustarnos definir funciones para las diferentes tareas. 1 #include <stdio.h> 2 3 #define FILAS 9 4 #define COLUMNAS 20 5 6 #define NO_SONDEADA ’+’ 7 8 void inicializa_tablero(char tablero[][COLUMNAS]) 9 /* Inicializa el tablero de juego marcando todas las casillas como no sondeadas. */ 10 { 11 int i, j; 12 13 for (i=0; i<FILAS; i++) 14 for (j=0; j<COLUMNAS; j++) 15 tablero[i][j] = NO_SONDEADA; Introducci´on a la Programaci´on con C 175
  • 182. 3.5 Paso de par´ametros 16 } 17 18 int main(void) 19 { 20 char espacio[FILAS][COLUMNAS]; 21 22 inicializa_tablero(espacio); 23 24 return 0; 25 } Pasamos la matriz indicando el n´umero de columnas de la misma.5 En el interior de la funci´on se modifica el contenido de la matriz. Los cambios afectar´an a la variable que suministremos como argumento, pues las matrices se pasan siempre por referencia. Hemos de mostrar por pantalla el contenido de la matriz en m´as de una ocasi´on. Podemos dise˜nar un procedimiento que se encargue de esta tarea: 1 #include <stdio.h> 2 3 #define FILAS 9 4 #define COLUMNAS 20 5 6 #define NO_SONDEADA ’+’ 7 8 ... 9 10 void muestra_tablero(char tablero[][COLUMNAS]) 11 /* Muestra en pantalla el tablero de juego. */ 12 { 13 int i, j; 14 15 // Etiquetar con una letra cada columna. 16 printf (" "); 17 for (j=0; j<COLUMNAS; j++) printf ("%c", ’A’+j); 18 printf ("n"); 19 20 for (i=0; i<FILAS; i++) { 21 printf ("%d ", i); // Etiqueta de cada fila. 22 for (j=0; j<COLUMNAS; j++) 23 printf ("%c", tablero[i][j]); 24 printf ("n"); 25 } 26 } 27 28 int main(void) 29 { 30 char espacio[FILAS][COLUMNAS]; 31 32 inicializa_tablero(espacio); 33 muestra_tablero(espacio); 34 35 return 0; 36 } El procedimiento muestra_tablero imprime, adem´as, del contenido del tablero, el nombre de las columnas y el n´umero de las filas. Por cierto, hay una discrepancia entre el modo con que nos referimos a las casillas (mediante un d´ıgito y una letra) y el modo con el que lo hace el programa (mediante dos n´umeros enteros). Cuando pidamos unas coordenadas al usuario lo haremos con una sentencia como ´esta: 5No hemos usado el nombre espacio, sino tablero, con el ´unico objetivo de resaltar que el par´ametro puede ser cualquier matriz (siempre que su dimensi´on se ajuste a lo esperado), aunque nosotros s´olo usaremos la matriz espacio como argumento. Si hubi´esemos usado el mismo nombre, es probable que hubi´esemos alimentado la confusi´on entre par´ametros y argumentos que experiment´ais algunos. 176 Introducci´on a la Programaci´on con C
  • 183. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones 1 ... 2 #define TALLACAD 80 3 ... 4 int main(void) 5 { 6 ... 7 char coordenadas[TALLACAD+1]; 8 9 ... 10 11 printf ("Coordenadas: "); scanf ("%s", coordenadas); 12 ... Como ves, las coordenadas se leer´an en una cadena. Nos convendr´a disponer, pues, de una funci´on que ((traduzca)) esa cadena a un par de n´umeros y otra que haga lo contrario: 1 void de_fila_y_columna_a_numero_y_letra(int fila, int columna, char * coordenadas) 2 /* Convierte una fila y columna descritas num´ericamente en una fila y columna descritas 3 * como una cadena con un d´ıgito y una letra. 4 */ 6 { 7 coordenadas[0] = ’0’ + fila; 8 coordenadas[1] = ’A’ + columna; 9 coordenadas[2] = ’0’; 10 } 11 12 int de_numero_y_letra_a_fila_y_columna(char coordenadas[], int * fila, int * columna) 13 /* Convierte una fila y columna con un d´ıgito y una letra (min´uscula o may´uscula) en 14 * cualquier orden a una fila y columna descritas num´ericamente. 15 */ 17 { 18 if (strlen(coordenadas) != 2) 19 return 0; 20 if (coordenadas[0] >= ’0’ && coordenadas[0] <= ’8’ && isalpha(coordenadas[1])) { 21 *fila = coordenadas[0] - ’0’; 22 *columna = toupper(coordenadas[1]) - ’A’; 23 return 1; 24 } 25 if (coordenadas[1] >= ’0’ && coordenadas[1] <= ’8’ && isalpha(coordenadas[0])) { 26 *columna = toupper(coordenadas[0]) - ’A’; 27 *fila = coordenadas[1] - ’0’; 28 return 1; 29 } 30 return 0; 31 } La primera funci´on (de_fila_y_columna_a_numero_y_letra) es muy sencilla: recibe el valor de la fila y el valor de la columna y modifica el contenido de un puntero a una cadena. Observa que es responsabilidad nuestra terminar correctamente la cadena coordenadas. La segunda funci´on es algo m´as complicada. Una raz´on para ello es que efect´ua cierto tratamiento de errores. ¿Por qu´e? Porque la cadena coordenadas ha sido introducida por el usuario y puede contener errores. Usamos un convenio muy frecuente en los programas C: Los valores se devuelven en la funci´on mediante par´ametros pasados por referencia, y la funci´on devuelve un valor que indica si se detect´o o no un error (devuelve 0 si hubo error, y 1 en caso contrario). De este modo es posible invocar a la funci´on cuando leemos el contenido de la cadena de esta forma: 1 ... 2 printf ("Coordenadas: "); scanf ("%s", coordenadas); 3 while (!de_numero_y_letra_a_fila_y_columna(coordenadas, &fila, &columna)) { 4 printf ("Coordenadas no v´alidas. Int´entelo de nuevo.nCoordenadas: "); Introducci´on a la Programaci´on con C 177
  • 184. 3.5 Paso de par´ametros 5 scanf ("%s", coordenadas); 6 } 7 ... Sigamos. Hemos de disponer ahora 5 n´aufragos en el tablero de juego. Podr´ıamos ponerlos directamente en la matriz espacio modificando el valor de las casillas pertinentes, pero en tal caso muestra_tablero los mostrar´ıa, revelando el secreto de su posici´on y reduciendo notablemente el inter´es del juego ;-). ¿Qu´e hacer? Una posibilidad consiste en usar una matriz adicional en la que poder disponer los n´aufragos. Esta nueva matriz no se mostrar´ıa nunca al usuario y ser´ıa consultada por el programa cuando se necesitara saber si hay un n´aufrago en alguna posici´on determinada del tablero. Si bien es una posibilidad interesante (y te la propondremos m´as adelante como ejercicio), nos decantamos por seguir una diferente que nos permitir´a practicar el paso de registros a funciones. Definiremos los siguientes registros: ... #define MAX_NAUFRAGOS 5 struct Naufrago { int fila, columna; // Coordenadas int encontrado; // ? Ha sido encontrado ya? }; struct GrupoNaufragos { struct Naufrago naufrago[MAX_NAUFRAGOS]; int cantidad; }; ... El tipo registro struct Naufrago mantiene la posici´on de un n´aufrago y permite saber si sigue perdido o si, por el contrario, ya ha sido encontrado. El tipo registro struct GrupoNaufragos mantiene un vector de n´aufragos de talla MAX_NAUFRAGOS. Aunque el juego indica que hemos de trabajar con 5 n´aufragos, usaremos un campo adicional con la cantidad de n´aufragos realmente almacenados en el vector. De ese modo resultar´a sencillo modificar el juego (como te propone- mos en los ejercicios al final de esta secci´on) para que se juegue con un n´umero de n´aufragos seleccionado por el usuario. Guardaremos los n´aufragos en una variable de tipo struct GrupoNaufragos: 1 ... 2 3 int main(void) 4 { 5 char espacio[FILAS][COLUMNAS]; 6 struct GrupoNaufragos losNaufragos; 7 8 inicializa_tablero(espacio); 9 muestra_tablero(espacio); 10 11 return 0; 12 } El programa deber´ıa empezar realmente por inicializar el registro losNaufragos ubicando a cada n´aufrago en una posici´on aletoria del tablero. Esta funci´on (err´onea) se encarga de ello: ... #include <stdlib.h> ... void pon_naufragos(struct GrupoNaufragos * grupoNaufragos, int cantidad) /* Situa aleatoriamente cantidad n´aufragos en la estructura grupoNaufragos. */ /* PERO LO HACE MAL. */ { int fila, columna; grupoNaufragos->cantidad = 0; 178 Introducci´on a la Programaci´on con C
  • 185. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones while (grupoNaufragos->cantidad != cantidad) { fila = rand() % FILAS; columna = rand() % COLUMNAS; grupoNaufragos->naufrago[grupoNaufragos->cantidad].fila = fila; grupoNaufragos->naufrago[grupoNaufragos->cantidad].columna = columna; grupoNaufragos->naufrago[grupoNaufragos->cantidad].encontrado = 0; grupoNaufragos->cantidad++; } } ¿Por qu´e est´a mal? Primero hemos de entenderla bien. Analic´emosla paso a paso. Empecemos por la cabecera: la funci´on tiene dos par´ametros, uno que es una referencia (un puntero) a un registro de tipo struct GrupoNaufragos y un entero que nos indica cu´antos n´aufragos hemos de poner al azar. La rutina empieza inicializando a cero la cantidad de n´aufragos ya dispuestos mediante una l´ınea como ´esta: grupoNaufragos ->cantidad = 0; ¿Entiendes por qu´e se usa el operador flecha?: la variable grupoNaufragos es un puntero, as´ı que hemos de acceder a la informaci´on apuntada antes de acceder al campo cantidad. Podr´ıamos haber escrito esa misma l´ınea as´ı: (*grupoNaufragos ).cantidad = 0; pero hubiera resultado m´as inc´omodo (e ilegible). A continuaci´on, la funci´on repite cantidad veces la acci´on consistente en seleccionar una fila y columna al azar (mediante la funci´on rand de stdlib.h) y lo anota en una posici´on del vector de n´aufragos. Puede que esta l´ınea te resulte un tanto dif´ıcil de entender: grupoNaufragos->naufrago[grupoNaufragos->cantidad ].fila = fila; pero no lo es tanto si la analizas paso a paso. Veamos. Empecemos por el ´ındice que hemos sombreado arriba. La primera vez, es 0, la segunda 1, y as´ı sucesivamente. En aras de comprender la sentencia, nos conviene reescribir la sentencia poniendo de momento un 0 en el ´ındice: grupoNaufragos->naufrago[0].fila = fila; M´as claro, ¿no? Piensa que grupoNaufragos->naufrago es un vector como cualquier otro, as´ı que la expresi´on grupoNaufragos->naufrago[0] accede a su primer elemento. ¿De qu´e tipo es ese elemento? De tipo struct Naufrago. Un elemento de ese tipo tiene un campo fila y se accede a ´el con el operador punto. O sea, esa sentencia asigna el valor de fila al campo fila de un elemento del vector naufrago del registro que es apuntado por grupoNaufragos. El resto de la funci´on te debe resultar f´acil de leer ahora. Volvamos a la cuesti´on principal: ¿por qu´e est´a mal dise˜nada esa funci´on? F´acil: porque puede ubicar dos n´aufragos en la misma casilla del tablero. ¿C´omo corregimos el problema? Asegur´andonos de que cada n´aufrago ocupa una casilla diferente. Tenemos dos posibilidades: Generar las posiciones de cinco n´aufragos al azar y comprobar que son todas diferentes entre s´ı. Si lo son, perfecto: hemos acabado; si no, volvemos a repetir todo el proceso. Ir generando la posici´on de cada n´aufrago de una en una, comprobando cada vez que ´esta es distinta de la de todos los n´aufragos anteriores. Si no lo es, volvemos a generar la posici´on de este n´aufrago concreto; si lo es, pasamos al siguiente. La segunda resulta m´as sencilla de implementar y es, a la vez, m´as eficiente. Aqu´ı la tienes implementada: void pon_naufragos(struct GrupoNaufragos * grupoNaufragos, int cantidad) /* Sit´ua aleatoriamente cantidad n´aufragos en la estructura grupoNaufragos. */ { int fila, columna, ya_hay_uno_ahi, i; grupoNaufragos->cantidad = 0; while (grupoNaufragos->cantidad != cantidad) { fila = rand() % FILAS; columna = rand() % COLUMNAS; ya_hay_uno_ahi = 0; Introducci´on a la Programaci´on con C 179
  • 186. 3.5 Paso de par´ametros for (i=0; i<grupoNaufragos->cantidad; i++) if (fila == grupoNaufragos->naufrago[i].fila && columna == grupoNaufragos->naufrago[i].columna) { ya_hay_uno_ahi = 1; break; } if (!ya_hay_uno_ahi) { grupoNaufragos->naufrago[grupoNaufragos->cantidad].fila = fila; grupoNaufragos->naufrago[grupoNaufragos->cantidad].columna = columna; grupoNaufragos->naufrago[grupoNaufragos->cantidad].encontrado = 0; grupoNaufragos->cantidad++; } } } Nos vendr´a bien disponer de una funci´on que muestre por pantalla la ubicaci´on y estado de cada n´aufrago. Esta funci´on no resulta ´util para el juego (pues perder´ıa toda la gracia), pero s´ı para ayudarnos a depurar el programa. Podr´ıamos, por ejemplo, ayudarnos con llamadas a esa funci´on mientras jugamos partidas de prueba y, una vez dado por bueno el programa, no llamarla m´as. En cualquier caso, aqu´ı la tienes: void muestra_naufragos(struct GrupoNaufragos grupoNaufragos) /* Muestra por pantalla las coordenadas de cada n´aufrago e informa de si sigue perdido. * ´Util para depuraci´on del programa. */ { int i; char coordenadas[3]; for (i=0; i<grupoNaufragos.cantidad; i++) { de_fila_y_columna_a_numero_y_letra(grupoNaufragos.naufrago[i].fila, grupoNaufragos.naufrago[i].columna, coordenadas); printf ("N´aufrago %d en coordenadas %s ", i, coordenadas); if (grupoNaufragos.naufrago[i].encontrado) printf ("ya ha sido encontrado.n"); else printf ("sigue perdido.n"); } } La funci´on est´a bien, pero podemos mejorarla. F´ıjate en c´omo pasamos su par´ametro: por valor. ¿Por qu´e? Porque no vamos a modificar su valor en el interior de la funci´on. En principio, la decisi´on de pasarlo por valor est´a bien fundamentada. No obstante, piensa en qu´e ocurre cada vez que llamamos a la funci´on: como un registro de tipo struct GrupoNaufragos ocupa 64 bytes (haz cuentas y compru´ebalo), cada llamada a la funci´on obliga a copiar 64 bytes en la pila. El problema se agravar´ıa si en lugar de trabajar con un n´umero m´aximo de 5 n´aufragos lo hici´eramos con una cantidad mayor. ¿Es realmente necesario ese esfuerzo? La verdad es que no: podemos limitarnos a copiar 4 bytes si pasamos una referencia al registro. Esta nueva versi´on de la funci´on efect´ua el paso por referencia: void muestra_naufragos(struct GrupoNaufragos * grupoNaufragos) /* Muestra por pantalla las coordenadas de cada n´aufrago e informa de si sigue perdido. * ´Util para depuraci´on del programa. */ { int i, fila, columna; char coordenadas[3]; for (i=0; i<grupoNaufragos ->cantidad; i++) { de_fila_y_columna_a_numero_y_letra(grupoNaufragos ->naufrago[i].fila, grupoNaufragos ->naufrago[i].columna, coordenadas); printf ("N´aufrago %d en coordenadas %s ", i, coordenadas); if (grupoNaufragos ->naufrago[i].encontrado) 180 Introducci´on a la Programaci´on con C
  • 187. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones printf ("ya ha sido encontrado.n"); else printf ("sigue perdido.n"); } } Es posible usar el adjetivo const para dejar claro que pasamos el puntero por eficiencia, pero no porque vayamos a modificar su contenido: void muestra_naufragos(const struct GrupoNaufragos * grupoNaufragos) Hagamos una prueba para ver si todo va bien por el momento: 1 ... 2 3 int main(void) 4 { 5 struct GrupoNaufragos losNaufragos; 6 7 pon_naufragos(&losNaufragos, 5); 8 muestra_naufragos(&losNaufragos); 9 10 return 0; 11 } Compilemos y ejecutemos el programa. He aqu´ı el resultado: $ gcc minigalaxis.c -o minigalaxis $ minigalaxis N´aufrago 0 en coordenadas 1G sigue perdido. N´aufrago 1 en coordenadas 0P sigue perdido. N´aufrago 2 en coordenadas 5P sigue perdido. N´aufrago 3 en coordenadas 1M sigue perdido. N´aufrago 4 en coordenadas 6B sigue perdido. Bien: cada n´aufrago ocupa una posici´on diferente. Ejecut´emoslo de nuevo $ minigalaxis N´aufrago 0 en coordenadas 1G sigue perdido. N´aufrago 1 en coordenadas 0P sigue perdido. N´aufrago 2 en coordenadas 5P sigue perdido. N´aufrago 3 en coordenadas 1M sigue perdido. N´aufrago 4 en coordenadas 6B sigue perdido. ¡Eh! ¡Se han ubicado en las mismas posiciones! ¿Qu´e gracia tiene el juego si en todas las partidas aparecen los n´aufragos en las mismas casillas? ¿C´omo es posible que ocurra algo as´ı? ¿No se generaba su ubicaci´on al azar? S´ı y no. La funci´on rand genera n´umeros pseudoaleatorios. Utiliza una f´ormula matem´atica que genera una secuencia de n´umeros de forma tal que no podemos efectuar una predicci´on del siguiente (a menos que conozcamos la f´ormula, claro est´a). La secuencia de n´umeros se genera a partir de un n´umero inicial: la semilla. En principio, la semilla es siempre la misma, as´ı que la secuencia de n´umeros es, tambi´en, siempre la misma. ¿Qu´e hacer, pues, si queremos obtener una diferente? Una posibilidad es solicitar al usuario el valor de la semilla, que se puede modificar con la funci´on srand, pero no parece lo adecuado para un juego de ordenador (el usuario podr´ıa hacer trampa introduciendo siempre la misma semilla). Otra posibilidad es inicializar la semilla con un valor aleatorio. ¿Con un valor aleatorio? Tenemos un pez que se muerde la cola: ¡resulta que necesito un n´umero aleatorio para generar n´umeros aleatorios! Mmmmm. Tranquilo, hay una soluci´on: consultar el reloj del ordenador y usar su valor como semilla. La funci´on time (disponible incluyendo time.h) nos devuelve el n´umero de segundos transcurridos desde el inicio del d´ıa 1 de enero de 1970 (lo que se conoce por tiempo de la era Unix) y, naturalmente, es diferente cada vez que lo llamamos para iniciar una partida. Aqu´ı tienes la soluci´on: 1 ... 2 #include <time.h> 3 ... 4 Introducci´on a la Programaci´on con C 181
  • 188. 3.5 Paso de par´ametros 5 int main(void) 6 { 7 struct GrupoNaufragos losNaufragos; 8 9 srand(time(0)); 10 11 pon_naufragos(&losNaufragos, 5); 12 muestra_naufragos(&losNaufragos); 13 14 return 0; 15 } Efectuemos nuevas pruebas: $ gcc minigalaxis.c -o minigalaxis $ minigalaxis N´aufrago 0 en coordenadas 6K sigue perdido. N´aufrago 1 en coordenadas 5L sigue perdido. N´aufrago 2 en coordenadas 6E sigue perdido. N´aufrago 3 en coordenadas 3I sigue perdido. N´aufrago 4 en coordenadas 8T sigue perdido. ¡Bravo! Son valores diferentes de los anteriores. Ejecutemos nuevamente el programa: $ minigalaxis N´aufrago 0 en coordenadas 2D sigue perdido. N´aufrago 1 en coordenadas 4H sigue perdido. N´aufrago 2 en coordenadas 5J sigue perdido. N´aufrago 3 en coordenadas 4E sigue perdido. N´aufrago 4 en coordenadas 7G sigue perdido. ¡Perfecto! A otra cosa. Ya hemos inicializado el tablero y dispuesto los n´aufragos en posiciones al azar. Dise˜nemos una funci´on para el lanzamiento de sondas. La funci´on (que ser´a un procedimiento) recibir´a un par de coordenadas, el tablero de juego y el registro que contiene la posici´on de los n´aufragos y har´a lo siguiente: modificar´a el tablero de juego sustituyendo los s´ımbolos ’+’ por ’.’ en las direcciones cardinales desde el punto de lanzamiento de la sonda, y modificar´a la casilla en la que se lanz´o la sonda indicando el n´umero de n´aufragos detectados, o marc´andola con una ’X’ si hay un n´aufrago en ella. 1 ... 2 #define NO_SONDEADA ’+’ 3 #define RESCATADO ’X’ 4 #define SONDEADA ’.’ 5 ... 6 7 void lanzar_sonda(int fila, int columna, char tablero[][COLUMNAS], 8 const struct GrupoNaufragos * grupoNaufragos) 9 /* Lanza una sonda en las coordenadas indicadas. Actualiza el tablero con el resultado del 10 * sondeo. Si se detecta un n´aufrago en el punto de lanzamiento de la sonda, lo rescata. 11 */ 13 { 14 int detectados = 0, i; 15 16 // Recorrer la vertical 17 for (i=0; i<FILAS; i++) { 18 if (hay_naufrago(i, columna, grupoNaufragos)) 19 detectados++; 20 if (tablero[i][columna] == NO_SONDEADA) 21 tablero[i][columna] = SONDEADA; 182 Introducci´on a la Programaci´on con C
  • 189. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones 22 } 23 24 // Recorrer la horizontal 25 for (i=0; i<COLUMNAS; i++) { 26 if (hay_naufrago(fila, i, grupoNaufragos)) 27 detectados++; 28 if (tablero[fila][i] == NO_SONDEADA) 29 tablero[fila][i] = SONDEADA; 30 } 31 32 // Ver si acertamos y hay un n´aufrago en esta misma casilla. 33 if (hay_naufrago(fila, columna, grupoNaufragos)) { 34 tablero[fila][columna] = RESCATADO; // En tal caso, ponemos una X. 35 rescate(fila, columna, grupoNaufragos); 36 } 37 else 38 tablero[fila][columna] = ’0’ + detectados; // Y si no, el n´umero de n´aufragos detectados. 39 } Esta funci´on se ayuda con otras dos: hay_naufrago y rescate. La primera nos indica si hay un n´aufrago en una casilla determinada: 1 int hay_naufrago(int fila, int columna, const struct GrupoNaufragos * grupoNaufragos) 2 /* Averigua si hay un n´aufrago perdido en las coordenadas (fila, columna). 3 * Si lo hay devuelve 1; si no lo hay, devuelve 0. 4 */ 6 { 7 int i; 8 9 for (i=0; i<grupoNaufragos->cantidad; i++) 10 if (fila == grupoNaufragos->naufrago[i].fila && 11 columna == grupoNaufragos->naufrago[i].columna) 12 return 1; 13 return 0; 14 } Y la segunda lo marca como rescatado: 1 void rescate(int fila, int columna, struct GrupoNaufragos * grupoNaufragos) 2 /* Rescata al n´aufrago que hay en las coordenadas indicadas. */ 3 { 4 int i; 5 6 for (i=0; i<grupoNaufragos->cantidad; i++) 7 if (fila == grupoNaufragos->naufrago[i].fila && 8 columna == grupoNaufragos->naufrago[i].columna) 9 grupoNaufragos->naufrago[i].encontrado = 1; 10 } Ya podemos ofrecer una versi´on m´as completa del programa principal: 1 int main(void) 2 { 3 char espacio[FILAS][COLUMNAS]; 4 struct GrupoNaufragos losNaufragos; 5 char coordenadas[TALLACAD+1]; 6 int fila, columna; 7 8 srand(time(0)); 9 10 pon_naufragos(&losNaufragos, 5); 11 inicializa_tablero(espacio); 12 muestra_tablero(espacio); 13 14 while ( ??? ) { Introducci´on a la Programaci´on con C 183
  • 190. 3.5 Paso de par´ametros 15 printf ("Coordenadas: "); scanf ("%s", coordenadas); 16 while (!de_numero_y_letra_a_fila_y_columna(coordenadas, &fila, &columna)) { 17 printf ("Coordenadas no v´alidas. Int´entelo de nuevo.nCoordenadas: "); 18 scanf ("%s", coordenadas); 19 } 20 lanzar_sonda(fila, columna, espacio, &losNaufragos); 21 muestra_tablero(espacio); 22 } 23 24 return 0; 25 } ¿Cu´ando debe finalizar el bucle while exterior? Bien cuando hayamos rescatado a todos los n´aufragos, bien cuando nos hayamos quedado sin sondas. En el primer caso habremos vencido y en el segundo habremos perdido: 1 ... 2 #define SONDAS 20 3 ... 4 5 int perdidos(const struct GrupoNaufragos * grupoNaufragos) 6 /* Cuenta el n´umero de n´aufragos que siguen perdidos. */ 7 { 8 int contador = 0, i; 9 10 for (i=0; i<grupoNaufragos->cantidad; i++) 11 if (!grupoNaufragos->naufrago[i].encontrado) 12 contador++; 13 return contador; 14 } 15 16 ... 17 18 int main(void) 19 { 20 char espacio[FILAS][COLUMNAS]; 21 struct GrupoNaufragos losNaufragos; 22 int sondas_disponibles = SONDAS; 23 char coordenadas[TALLACAD+1]; 24 int fila, columna; 25 26 srand(time(0)); 27 28 pon_naufragos(&losNaufragos, 5); 29 inicializa_tablero(espacio); 30 muestra_tablero(espacio); 31 32 while (sondas_disponibles > 0 && perdidos(&losNaufragos) > 0) { 33 printf ("Hay %d n´aufragosn", perdidos(&losNaufragos)); 34 printf ("Dispones de %d sondasn", sondas_disponibles); 35 printf ("Coordenadas: "); scanf ("%s", coordenadas); 36 while (!de_numero_y_letra_a_fila_y_columna(coordenadas, &fila, &columna)) { 37 printf ("Coordenadas no v´alidas. Int´entelo de nuevo.nCoordenadas: "); 38 scanf ("%s", coordenadas); 39 } 40 lanzar_sonda(fila, columna, espacio, &losNaufragos); 41 muestra_tablero(espacio); 42 sondas_disponibles--; 43 } 44 45 if (perdidos(&losNaufragos) == 0) 46 printf ("Has ganado. Puntuaci´on: %d puntos.n", SONDAS - sondas_disponibles); 47 else 48 printf ("Has perdido. Por tu culpa han muerto %d n´aufragosn", 184 Introducci´on a la Programaci´on con C
  • 191. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones 49 perdidos(&losNaufragos)); 50 51 return 0; 52 } Hemos definido una nueva funci´on, perdidos, que calcula el n´umero de n´aufragos que per- manecen perdidos. Y ya est´a. Te mostramos finalmente el listado completo del programa: minigalaxis.c minigalaxis.c 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <ctype.h> 5 #include <time.h> 6 7 #define FILAS 9 8 #define COLUMNAS 20 9 #define TALLACAD 80 10 #define MAX_NAUFRAGOS 5 11 #define SONDAS 20 12 13 #define NO_SONDEADA ’+’ 14 #define RESCATADO ’X’ 15 #define SONDEADA ’.’ 16 17 /********************************************************** 18 * Conversi´on entre los dos modos de expresar coordenadas 19 **********************************************************/ 21 22 void de_fila_y_columna_a_numero_y_letra(int fila, int columna, char coordenadas[]) 23 /* Convierte una fila y columna descritas num´ericamente en una fila y columna descritas 24 * como una cadena con un d´ıgito y una letra. 25 */ 27 { 28 coordenadas[0] = ’0’ + fila; 29 coordenadas[1] = ’A’ + columna; 30 coordenadas[2] = ’0’; 31 } 32 33 int de_numero_y_letra_a_fila_y_columna(char coordenadas[], int * fila, int * columna) 34 /* Convierte una fila y columna con un d´ıgito y una letra (min´uscula o may´uscula) en 35 * cualquier orden a una fila y columna descritas num´ericamente. 36 */ 38 { 39 printf (">>> %sn", coordenadas); 40 if (strlen(coordenadas) != 2) 41 return 0; 42 if (coordenadas[0] >= ’0’ && coordenadas[0] <= ’8’ && isalpha(coordenadas[1])) { 43 *fila = coordenadas[0] - ’0’; 44 *columna = toupper(coordenadas[1]) - ’A’; 45 return 1; 46 } 47 if (coordenadas[1] >= ’0’ && coordenadas[1] <= ’8’ && isalpha(coordenadas[0])) { 48 *columna = toupper(coordenadas[0]) - ’A’; 49 *fila = coordenadas[1] - ’0’; 50 return 1; 51 } 52 return 0; 53 } 54 55 /**************************************** 56 * N´aufragos 57 ****************************************/ Introducci´on a la Programaci´on con C 185
  • 192. 3.5 Paso de par´ametros 59 60 struct Naufrago { 61 int fila, columna; // Coordenadas 62 int encontrado; // ? Ha sido encontrado ya? 63 }; 64 65 struct GrupoNaufragos { 66 struct Naufrago naufrago[MAX_NAUFRAGOS]; 67 int cantidad; 68 }; 69 70 void pon_naufragos(struct GrupoNaufragos * grupoNaufragos, int cantidad) 71 /* Situa aleatoriamente cantidad n´aufragos en la estructura grupoNaufragos. */ 72 { 73 int fila, columna, ya_hay_uno_ahi, i; 74 75 grupoNaufragos->cantidad = 0; 76 while (grupoNaufragos->cantidad != cantidad) { 77 fila = rand() % FILAS; 78 columna = rand() % COLUMNAS; 79 ya_hay_uno_ahi = 0; 80 for (i=0; i<grupoNaufragos->cantidad; i++) 81 if (fila == grupoNaufragos->naufrago[i].fila && 82 columna == grupoNaufragos->naufrago[i].columna) { 83 ya_hay_uno_ahi = 1; 84 break; 85 } 86 if (!ya_hay_uno_ahi) { 87 grupoNaufragos->naufrago[grupoNaufragos->cantidad].fila = fila; 88 grupoNaufragos->naufrago[grupoNaufragos->cantidad].columna = columna; 89 grupoNaufragos->naufrago[grupoNaufragos->cantidad].encontrado = 0; 90 grupoNaufragos->cantidad++; 91 } 92 } 93 } 94 95 int hay_naufrago(int fila, int columna, const struct GrupoNaufragos * grupoNaufragos) 96 /* Averigua si hay un n´aufrago perdido en las coordenadas (fila, columna). 97 * Si lo hay devuelve 1; si no lo hay, devuelve 0. 98 */ 100 { 101 int i; 102 103 for (i=0; i<grupoNaufragos->cantidad; i++) 104 if (fila == grupoNaufragos->naufrago[i].fila && 105 columna == grupoNaufragos->naufrago[i].columna) 106 return 1; 107 return 0; 108 } 109 110 111 void rescate(int fila, int columna, struct GrupoNaufragos * grupoNaufragos) 112 /* Rescata al n´aufrago que hay en las coordenadas indicadas. */ 113 { 114 int i; 115 116 for (i=0; i<grupoNaufragos->cantidad; i++) 117 if (fila == grupoNaufragos->naufrago[i].fila && 118 columna == grupoNaufragos->naufrago[i].columna) 119 grupoNaufragos->naufrago[i].encontrado = 1; 120 } 121 122 int perdidos(const struct GrupoNaufragos * grupoNaufragos) 186 Introducci´on a la Programaci´on con C
  • 193. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones 123 /* Cuenta el n´umero de n´aufragos que siguen perdidos. */ 124 { 125 int contador = 0, i; 126 127 for (i=0; i<grupoNaufragos->cantidad; i++) 128 if (!grupoNaufragos->naufrago[i].encontrado) 129 contador++; 130 return contador; 131 } 132 133 void muestra_naufragos(const struct GrupoNaufragos * grupoNaufragos) 134 /* Muestra por pantalla las coordenadas de cada naufrago e informa de si sigue perdido. 135 * ´Util para depuraci´on del programa. 136 */ 138 { 139 int i; 140 char coordenadas[3]; 141 142 for (i=0; i<grupoNaufragos->cantidad; i++) { 143 de_fila_y_columna_a_numero_y_letra(grupoNaufragos->naufrago[i].fila, 144 grupoNaufragos->naufrago[i].columna, 145 coordenadas); 146 printf ("Naufrago %d en coordenadas %s ", i, coordenadas); 147 if (grupoNaufragos->naufrago[i].encontrado) 148 printf ("ya ha sido encontrado.n"); 149 else 150 printf ("sigue perdido.n"); 151 } 152 } 153 154 /**************************************** 155 * Tablero 156 ****************************************/ 158 159 void inicializa_tablero(char tablero[][COLUMNAS]) 160 /* Inicializa el tablero de juego marcando todas las casillas como no sondeadas. */ 161 { 162 int i, j; 163 164 for (i=0; i<FILAS; i++) 165 for (j=0; j<COLUMNAS; j++) 166 tablero[i][j] = NO_SONDEADA; 167 } 168 169 void muestra_tablero(char tablero[][COLUMNAS]) 170 /* Muestra en pantalla el tablero de juego. */ 171 { 172 int i, j; 173 174 // Etiquetar con una letra cada columna. 175 printf (" "); 176 for (j=0; j<COLUMNAS; j++) printf ("%c", ’A’+j); 177 printf ("n"); 178 179 for (i=0; i<FILAS; i++) { 180 printf ("%d ", i); // Etiqueta de cada fila. 181 for (j=0; j<COLUMNAS; j++) 182 printf ("%c", tablero[i][j]); 183 printf ("n"); 184 } 185 } 186 187 /**************************************** Introducci´on a la Programaci´on con C 187
  • 194. 3.5 Paso de par´ametros 188 * Sonda 189 ****************************************/ 191 192 void lanzar_sonda(int fila, int columna, char tablero[][COLUMNAS], 193 struct GrupoNaufragos * grupoNaufragos) 194 /* Lanza una sonda en las coordenadas indicadas. Actualiza el tablero con el resultado del 195 * sondeo. Si se detecta un n´aufrago en el punto de lanzamiento de la sonda, lo rescata. 196 */ 198 { 199 int detectados = 0, i; 200 201 // Recorrer la vertical 202 for (i=0; i<FILAS; i++) { 203 if (hay_naufrago(i, columna, grupoNaufragos)) 204 detectados++; 205 if (tablero[i][columna] == NO_SONDEADA) 206 tablero[i][columna] = SONDEADA; 207 } 208 209 // Recorrer la horizontal 210 for (i=0; i<COLUMNAS; i++) { 211 if (hay_naufrago(fila, i, grupoNaufragos)) 212 detectados++; 213 if (tablero[fila][i] == NO_SONDEADA) 214 tablero[fila][i] = SONDEADA; 215 } 216 217 // Ver si acertamos y hay una n´aufrago en esta misma casilla. 218 if (hay_naufrago(fila, columna, grupoNaufragos)) { 219 tablero[fila][columna] = RESCATADO; // En tal caso, ponemos una X. 220 rescate(fila, columna, grupoNaufragos); 221 } 222 else 223 tablero[fila][columna] = ’0’ + detectados; // Y si no, el n´umero de n´aufragos detectados. 224 } 225 226 int main(void) 227 { 228 char espacio[FILAS][COLUMNAS]; 229 struct GrupoNaufragos losNaufragos; 230 int sondas_disponibles = SONDAS; 231 char coordenadas[TALLACAD+1]; 232 int fila, columna; 233 234 srand(time(0)); 235 236 pon_naufragos(&losNaufragos, 5); 237 inicializa_tablero(espacio); 238 muestra_tablero(espacio); 239 240 while (sondas_disponibles > 0 && perdidos(&losNaufragos) > 0) { 241 printf ("Hay %d n´aufragosn", perdidos(&losNaufragos)); 242 printf ("Dispones de %d sondasn", sondas_disponibles); 243 printf ("Coordenadas: "); scanf ("%s", coordenadas); 244 while (!de_numero_y_letra_a_fila_y_columna(coordenadas, &fila, &columna)) { 245 printf ("Coordenadas no v´alidas. Int´entelo de nuevo.nCoordenadas: "); 246 scanf ("%s", coordenadas); 247 } 248 lanzar_sonda(fila, columna, espacio, &losNaufragos); 249 muestra_tablero(espacio); 250 sondas_disponibles--; 251 } 252 188 Introducci´on a la Programaci´on con C
  • 195. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones 253 if (perdidos(&losNaufragos) == 0) 254 printf ("Has ganado. Puntuaci´on: %d puntos.n", SONDAS - sondas_disponibles); 255 else 256 printf ("Has perdido. Por tu culpa han muerto %d n´aufragosn", 257 perdidos(&losNaufragos)); 258 259 return 0; 260 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 202 Reescribe el programa para que no se use una variable de tipo struct GrupoNaufragos como almac´en del grupo de n´aufragos, sino una matriz paralela a la matriz espacio. Cada n´aufrago se representar´a con un ’*’ mientras permanezca perdido, y con una ’X’ cuando haya sido descubierto. · 203 Siempre que usamos rand en miniGalaxis calculamos un par de n´umeros aleatorios. Hemos definido un nuevo tipo y una funci´on: 1 struct Casilla { 2 int fila, columna; 3 }; 4 5 struct Casilla casilla_al_azar(void) 6 { 7 struct Casilla casilla; 8 9 casilla.fila = rand() % FILAS; 10 casilla.columna = rand() % COLUMNAS; 11 return casilla; 12 } Y proponemos usarlos as´ı: 1 void pon_naufragos(struct GrupoNaufragos * grupoNaufragos, int cantidad) 2 /* Situa aleatoriamente cantidad n´aufragos en la estructura grupoNaufragos. */ 3 { 4 int fila, columna, ya_hay_uno_ahi, i; 5 struct Casilla una_casilla; 6 7 grupoNaufragos->cantidad = 0; 8 while (grupoNaufragos->cantidad != cantidad) { 9 una_casilla = casilla_al_azar(); 10 ya_hay_uno_ahi = 0; 11 for (i=0; i<grupoNaufragos->cantidad; i++) 12 if (una_casilla.fila == grupoNaufragos->naufrago[i].fila && 13 una_casilla.columna == grupoNaufragos->naufrago[i].columna) { 14 ya_hay_uno_ahi = 1; 15 break; 16 } 17 if (!ya_hay_uno_ahi) { 18 grupoNaufragos->naufrago[grupoNaufragos->cantidad].fila = una_casilla.fila ; 19 grupoNaufragos->naufrago[grupoNaufragos->cantidad].columna = una_casilla.columna ; 20 grupoNaufragos->naufrago[grupoNaufragos->cantidad].encontrado = 0; 21 grupoNaufragos->cantidad++; 22 } 23 } 24 } ¿Es correcto el programa con estos cambios? · 204 Como siempre que usamos rand calculamos un par de n´umeros aleatorios, hemos mo- dificado el programa de este modo: 1 struct Naufrago naufrago_al_azar(void) 2 { Introducci´on a la Programaci´on con C 189
  • 196. 3.6 Recursi´on 3 struct Naufrago naufrago; 4 5 naufrago.fila = rand() % FILAS; 6 naufrago.columna = rand() % COLUMNAS; 7 naufrago.encontrado = 0; 8 return naufrago; 9 } 10 11 void pon_naufragos(struct GrupoNaufragos * grupoNaufragos, int cantidad) 12 /* Situa aleatoriamente cantidad n´aufragos en la estructura grupoNaufragos. */ 13 { 14 int fila, columna, ya_hay_uno_ahi, i; 15 struct Naufrago un_naufrago; 16 17 grupoNaufragos->cantidad = 0; 18 while (grupoNaufragos->cantidad != cantidad) { 19 un_naufrago = naufrago_al_azar(); 20 ya_hay_uno_ahi = 0; 21 for (i=0; i<grupoNaufragos->cantidad; i++) 22 if (un_naufrago.fila == grupoNaufragos->naufrago[i].fila && 23 un_naufrago.columna == grupoNaufragos->naufrago[i].columna) { 24 ya_hay_uno_ahi = 1; 25 break; 26 } 27 if (!ya_hay_uno_ahi) { 28 grupoNaufragos->naufrago[grupoNaufragos->cantidad] = un_naufrago ; 29 grupoNaufragos->cantidad++; 30 } 31 } 32 } ¿Es correcto el programa con estos cambios? · 205 Modifica el juego para que el usuario pueda escoger el nivel de dificultad. El usuario escoger´a el n´umero de n´aufragos perdidos (con un m´aximo de 20) y el n´umero de sondas disponibles. · 206 Hemos construido una versi´on simplificada de Galaxis. El juego original s´olo se dife- rencia de ´este en las direcciones exploradas por la sonda: as´ı como las sondas de miniGalaxis exploran 4 direcciones, las de Galaxis exploran 8. Te mostramos el resultado de lanzar nuestra primera sonda en las coordenadas 4J de un tablero de juego Galaxis: ABCDEFGHIJKLMNOPQRST 0 +++++.+++.+++.++++++ 1 ++++++.++.++.+++++++ 2 +++++++.+.+.++++++++ 3 ++++++++...+++++++++ 4 .........1.......... 5 ++++++++...+++++++++ 6 +++++++.+.+.++++++++ 7 ++++++.++.++.+++++++ 8 +++++.+++.+++.++++++ Implementa el juego Galaxis. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.6. Recursi´on Es posible definir funciones recursivas en C. La funci´on factorial de este programa, por ejemplo, define un c´alculo recursivo del factorial: factorial recursivo.c factorial recursivo.c 1 #include <stdio.h> 2 190 Introducci´on a la Programaci´on con C
  • 197. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones 3 int factorial (int n) 4 { 5 if (n<=1) 6 return 1; 7 else 8 return n * factorial (n-1); 9 } 10 11 int main(void) 12 { 13 int valor; 14 15 printf ("Dame un n´umero entero positivo: "); 16 scanf ("%d", &valor); 17 printf ("El factorial de %d vale: %dn", valor, factorial(valor)); 18 19 return 0; 20 } Nada nuevo. Ya conoces el concepto de recursi´on de Python. En C es lo mismo. Tiene inter´es, eso s´ı, que estudiemos brevemente el aspecto de la memoria en un instante dado. Por ejemplo, cuando llamamos a factorial(5), que ha llamado a factorial(4), que a su vez ha llamado a factorial(3), la pila presentar´a esta configuraci´on: main 5valor llamada desde l´ınea 17 factorial 5n llamada desde l´ınea 8 factorial 4n llamada desde l´ınea 8 factorial 3n . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 207 Dise˜na una funci´on que calcule recursivamente xn . La variable x ser´a de tipo float y n de tipo int. · 208 Dise˜na una funci´on recursiva que calcule el n-´esimo n´umero de Fibonacci. · 209 Dise˜na una funci´on recursiva para calcular el n´umero combinatorio n sobre m sabiendo que n n = 1, n 0 = 1, n m = n − 1 m + n − 1 m − 1 . · 210 Dise˜na un procedimiento recursivo llamado muestra_bin que reciba un n´umero en- tero positivo y muestre por pantalla su codificaci´on en binario. Por ejemplo, si llamamos a muestra_bin(5), por pantalla aparecer´a el texto ((101)). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.6.1. Un m´etodo recursivo de ordenaci´on: mergesort Vamos a estudiar ahora un m´etodo recursivo de ordenaci´on de vectores: mergesort (que se podr´ıa traducir por ordenaci´on por fusi´on o mezcla). Estudiemos primero la aproximaci´on que Introducci´on a la Programaci´on con C 191
  • 198. 3.6 Recursi´on sigue considerando un procedimiento equivalente para ordenar las 12 cartas de un palo de la baraja de cartas. La ordenaci´on por fusi´on de un palo de la baraja consiste en lo siguiente: Dividir el paquete de cartas en dos grupos de 6 cartas; ordenar por fusi´on el primer grupo de 6 cartas; ordenar por fusi´on el segundo grupo de 6 cartas; fundir los dos grupos, que ya est´an ordenados, tomando siempre la carta con n´umero menor de cualquiera de los dos grupos (que siempre ser´a la primera de uno de los dos grupos). Ya ves d´onde aparece la recursi´on, ¿no? Para ordenar 12 cartas por fusi´on hemos de ordenar dos grupos de 6 cartas por fusi´on. Y para ordenar cada grupo de 6 cartas por fusi´on tendremos que ordenar dos grupos de 3 cartas por fusi´on. Y para ordenar 3 grupos de cartas por fusi´on. . . ¿Cu´ando finaliza la recursi´on? Cuando nos enfrentemos a casos triviales. Ordenar un grupo de 1 sola carta es trivial: ¡siempre est´a ordenado! Desarrollemos un ejemplo de ordenaci´on de un vector con 16 elementos: 11 0 21 1 3 2 1 3 98 4 0 5 12 6 82 7 29 8 30 9 11 10 18 11 43 12 4 13 75 14 37 15 1. Empezamos separando el vector en dos ((subvectores)) de 8 elementos: 11 0 21 1 3 2 1 3 98 4 0 5 12 6 82 7 29 8 30 9 11 10 18 11 43 12 4 13 75 14 37 15 2. ordenamos por fusi´on el primer vector, con lo que obtenemos: 0 0 1 1 3 2 11 3 12 4 21 5 82 6 98 7 3. y ordenamos por fusi´on el segundo vector, con lo que obtenemos: 4 0 11 1 18 2 29 3 30 4 37 5 43 6 75 7 4. y ahora ((fundimos)) ambos vectores ordenados, obteniendo as´ı un ´unico vector ordenado: 0 0 1 1 3 2 4 3 11 4 11 5 12 6 18 7 21 8 29 9 30 10 37 11 43 12 75 13 82 14 98 15 La idea b´asica de la fusi´on es sencilla: se recorren ambos vectores de izquierda a derecha, seleccionando en cada momento el menor elemento posible. Los detalles del proceso de fusi´on son un tanto escabrosos, as´ı que lo estudiaremos con calma un poco m´as adelante. Podemos representar el proceso realizado con esta imagen gr´afica: 11 0 21 1 3 2 1 3 98 4 0 5 12 6 82 7 29 8 30 9 11 10 18 11 43 12 4 13 75 14 37 15 11 0 21 1 3 2 1 3 98 4 0 5 12 6 82 7 29 8 30 9 11 10 18 11 43 12 4 13 75 14 37 15 0 0 1 1 3 2 11 3 12 4 21 5 82 6 98 7 4 8 11 9 18 10 29 11 30 12 37 13 43 14 75 15 0 0 1 1 3 2 4 3 11 4 11 5 12 6 18 7 21 8 29 9 30 10 37 11 43 12 75 13 82 14 98 15 dividir el problema (de talla 16) en dos problemas (de talla 8), resolver independientemente cada problema y combinar ambas soluciones. 192 Introducci´on a la Programaci´on con C
  • 199. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones Est´a claro que hemos hecho ((trampa)): las l´ıneas de trazo discontinuo esconden un proceso complejo, pues la ordenaci´on de cada uno de los vectores de 8 elementos supone la ordenaci´on (recursiva) de dos vectores de 4 elementos, que a su vez. . . ¿Cu´ando acaba el proceso recursivo? Cuando llegamos a un caso trivial: la ordenaci´on de un vector que s´olo tenga 1 elemento. He aqu´ı el proceso completo: 11 0 21 1 3 2 1 3 98 4 0 5 12 6 82 7 29 8 30 9 11 10 18 11 43 12 4 13 75 14 37 15 11 0 21 1 3 2 1 3 98 4 0 5 12 6 82 7 29 8 30 9 11 10 18 11 43 12 4 13 75 14 37 15 11 0 21 1 3 2 1 3 98 4 0 5 12 6 82 7 29 8 30 9 11 10 18 11 43 12 4 13 75 14 37 15 11 0 21 1 3 2 1 3 98 4 0 5 12 6 82 7 29 8 30 9 11 10 18 11 43 12 4 13 75 14 37 15 11 0 21 1 3 2 1 3 98 4 0 5 12 6 82 7 29 8 30 9 11 10 18 11 43 12 4 13 75 14 37 15 11 0 21 1 1 2 3 3 0 4 98 5 12 6 82 7 29 8 30 9 11 10 18 11 4 12 43 13 37 14 75 15 1 0 3 1 11 2 21 3 0 4 12 5 82 6 98 7 11 8 18 9 29 10 30 11 4 12 37 13 43 14 75 15 0 0 1 1 3 2 11 3 12 4 21 5 82 6 98 7 4 8 11 9 18 10 29 11 30 12 37 13 43 14 75 15 0 0 1 1 3 2 4 3 11 4 11 5 12 6 18 7 21 8 29 9 30 10 37 11 43 12 75 13 82 14 98 15 DivisionesFusiones Nos queda por estudiar con detalle el proceso de fusi´on. Desarrollemos primero una funci´on que recoja la idea b´asica de la ordenaci´on por fusi´on: se llamar´a mergesort y recibir´a un vector v y, en principio, la talla del vector que deseamos ordenar. Esta funci´on utilizar´a una funci´on auxiliar merge encargada de efectuar la fusi´on de vectores ya ordenados. Aqu´ı tienes un borrador incompleto: 1 void mergesort(int v[], int talla) 2 { 3 if (talla == 1) 4 return; 5 else { 6 mergesort ( la primera mitad de v ); 7 mergesort ( la segunda mitad de v ); 8 merge(la primera mitad de v, la segunda mitad de v); 9 } 10 } Dejemos para m´as adelante el desarrollo de merge. De momento, el principal problema es c´omo expresar lo de ((la primera mitad de v)) y ((la segunda mitad de v)). F´ıjate: en el fondo, se trata de se˜nalar una serie de elementos consecutivos del vector v. Cuando orden´abamos el vector del ejemplo ten´ıamos: 11 0 21 1 3 2 1 3 98 4 0 5 12 6 82 7 29 8 30 9 11 10 18 11 43 12 4 13 75 14 37 15 El primer ((subvector)) es la serie de valores entre el primer par de flechas, y el segundo ((subvector)) es la serie entre el segundo par de flechas. Modifiquemos, pues, mergesort para que Introducci´on a la Programaci´on con C 193
  • 200. 3.6 Recursi´on trabaje con ((subvectores)), es decir, con un vector e ´ındices que se˜nalan d´onde empieza y d´onde acaba cada serie de valores. 1 void mergesort(int v[], int inicio , int final ) 2 { 3 if (final - inicio == 0) 4 return; 5 else { 6 mergesort ( v, inicio , (inicio+final) / 2); 7 mergesort ( v, (inicio+final) / 2 + 1, final ); 8 merge(la primera mitad de v, la segunda mitad de v); 9 } 10 } Perfecto. Acabamos de expresar la idea de dividir un vector en dos sin necesidad de utilizar nuevos vectores. Nos queda por detallar la funci´on merge. Dicha funci´on recibe dos ((subvectores)) contiguos ya ordenados y los funde, haciendo que la zona de memoria que ambos ocupan pase a estar completamente ordenada. Este gr´afico muestra c´omo se fundir´ıan, paso a paso, dos vectores, a y b para formar un nuevo vector c. Necesitamos tres ´ındices, i, j y k, uno para cada vector: 1 0 3 1 11 2 21 3 0 0 12 1 82 2 98 3 0 1 2 3 4 5 6 7 i j k Inicialmente, los tres ´ındices valen 0. Ahora comparamos a[i] con b[j], seleccionamos el menor y almacenamos el valor en c[k]. Es necesario incrementar i si escogimos un elemento de a y j si lo escogimos de b. En cualquier caso, hemos de incrementar tambi´en la variable k: 1 0 3 1 11 2 21 3 0 0 12 1 82 2 98 3 0 0 1 2 3 4 5 6 7 i j k El proceso se repite hasta que alguno de los dos primeros ´ındices, i o j, se ((sale)) del vector correspondiente, tal y como ilustra esta secuencia de im´agenes: 1 0 3 1 11 2 21 3 0 0 12 1 82 2 98 3 0 0 1 1 2 3 4 5 6 7 i j k 1 0 3 1 11 2 21 3 0 0 12 1 82 2 98 3 0 0 1 1 3 2 3 4 5 6 7 i j k 1 0 3 1 11 2 21 3 0 0 12 1 82 2 98 3 0 0 1 1 3 2 11 3 4 5 6 7 i j k 1 0 3 1 11 2 21 3 0 0 12 1 82 2 98 3 0 0 1 1 3 2 11 3 12 4 5 6 7 i j k 1 0 3 1 11 2 21 3 0 0 12 1 82 2 98 3 0 0 1 1 3 2 11 3 12 4 21 5 6 7 i j k Ahora, basta con copiar los ´ultimos elementos del otro vector al final de c: 194 Introducci´on a la Programaci´on con C
  • 201. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones 1 0 3 1 11 2 21 3 0 0 12 1 82 2 98 3 0 0 1 1 3 2 11 3 12 4 21 5 82 6 7 i j k 1 0 3 1 11 2 21 3 0 0 12 1 82 2 98 3 0 0 1 1 3 2 11 3 12 4 21 5 82 6 98 7 i j k Un ´ultimo paso del proceso de fusi´on deber´ıa copiar los elementos de c en a y b, que en realidad son fragmentos contiguos de un mismo vector. Vamos a por los detalles de implementaci´on. No trabajamos con dos vectores independientes, sino con un s´olo vector en el que se marcan ((subvectores)) con pares de ´ındices. 1 void merge(int v[], int inicio1, int final1, int inicio2, int final2) 2 { 3 int i, j, k; 4 int c[final2-inicio1+1]; // Vector de talla determinada en tiempo de ejecuci´on. 5 6 i = inicio1; 7 j = inicio2; 8 k = 0; 9 10 while (i<=final1 && j<=final2) 11 if (v[i] < v[j]) 12 c[k++] = v[i++]; 13 else 14 c[k++] = v[j++]; 15 16 while (i<=final1) 17 c[k++] = v[i++]; 18 19 while (j<=final2) 20 c[k++] = v[j++]; 21 22 for (k=0; k<final2-inicio1+1; k++) 23 v[inicio1+k] = c[k]; 24 } El ´ultimo paso del procedimiento se encarga de copiar los elementos de c en el vector original. Ya est´a. Bueno, a´un podemos efectuar una mejora para reducir el n´umero de par´ametros: f´ıjate en que inicio2 siempre es igual a final1+1. Podemos prescindir de uno de los dos par´ametros: 1 void merge(int v[], int inicio1, int final1, int final2) 2 { 3 int i, j, k; 4 int c[final2-inicio1+1]; 5 6 i = inicio1; 7 j = final1+1; 8 k = 0; 9 10 while (i<=final1 && j<=final2) 11 if (v[i] < v[j]) 12 c[k++] = v[i++]; 13 else 14 c[k++] = v[j++]; 15 16 while (i<=final1) 17 c[k++] = v[i++]; 18 19 while (j<=final2) 20 c[k++] = v[j++]; Introducci´on a la Programaci´on con C 195
  • 202. 3.6 Recursi´on 21 22 for (k=0; k<final2-inicio1+1; k++) 23 v[inicio1+k] = c[k]; 24 } Veamos c´omo quedar´ıa un programa completo que use mergesort: ordena.c ordena.c 1 #include <stdio.h> 2 3 #define TALLA 100 4 5 void merge(int v[], int inicio1, int final1, int final2) 6 { 7 int i, j, k; 8 int c[final2-inicio1+1]; 9 10 i = inicio1; 11 j = final1+1; 12 k = 0; 13 14 while (i<=final1 && j<=final2) 15 if (v[i] < v[j]) 16 c[k++] = v[i++]; 17 else 18 c[k++] = v[j++]; 19 20 while (i<=final1) 21 c[k++] = v[i++]; 22 23 while (j<=final2) 24 c[k++] = v[j++]; 25 26 for (k=0; k<final2-inicio1+1; k++) 27 v[inicio1+k] = c[k]; 28 } 29 30 void mergesort(int v[], int inicio, int final) 31 { 32 if (final - inicio == 0) 33 return; 34 else { 35 mergesort ( v, inicio, (inicio+final) / 2 ); 36 mergesort ( v, (inicio+final) / 2 + 1, final ); 37 merge( v, inicio, (inicio+final) / 2, final ); 38 } 39 } 40 41 int main(void) 42 { 43 int mivector[TALLA]; 44 int i, talla; 45 46 talla = 0; 47 for (i=0; i<TALLA; i++) { 48 printf ("Introduce elemento %d (negativo para acabar): ", i); 49 scanf ("%d", &mivector[i]); 50 if (mivector[i] < 0) 51 break; 52 talla++; 53 } 54 55 mergesort(mivector, 0, talla-1); 56 196 Introducci´on a la Programaci´on con C
  • 203. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones 57 printf ("Vector ordenado:n"); 58 for (i=0; i<talla; i++) 59 printf ("%d ", mivector[i]); 60 printf ("n"); 61 return 0; 62 } He aqu´ı una ejecuci´on del programa: Introduce elemento 0 (negativo para acabar): 3 Introduce elemento 1 (negativo para acabar): 53 Introduce elemento 2 (negativo para acabar): 32 Introduce elemento 3 (negativo para acabar): 34 Introduce elemento 4 (negativo para acabar): 64 Introduce elemento 5 (negativo para acabar): 3 Introduce elemento 6 (negativo para acabar): 4 Introduce elemento 7 (negativo para acabar): 6 Introduce elemento 8 (negativo para acabar): 7 Introduce elemento 9 (negativo para acabar): -1 Vector ordenado: 3 3 4 6 7 32 34 53 64 Mergesort y el estilo C Los programadores C tienden a escribir los programas de una forma muy compacta. Estudia esta nueva versi´on de la funci´on merge: 1 void merge(int v[], int inicio1, int final1, int final2) 2 { 3 int i, j, k; 4 int c[final2-inicio1+1]; 5 6 for (i=inicio1, j=final1+1, k=0; i<=final1 && j<=final2; ) 7 c[k++] = (v[i] < v[j]) ? v[i++] : v[j++]; 8 while (i<=final1) c[k++] = v[i++]; 9 while (j<=final2) c[k++] = v[j++]; 10 for (k=0; k<final2-inicio1+1; k++) v[inicio1+k] = c[k]; 11 } Observa que los bucles for aceptan m´as de una inicializaci´on (separ´andolas por comas) y permiten que alguno de sus elementos est´e en blanco (en el primer for la acci´on de incremento del´ındice est´a en blanco). No te sugerimos que hagas t´u lo mismo: te prevenimos para que est´es preparado cuando te enfrentes a la lectura de programas C escritos por otros. Tambi´en vale la pena apreciar el uso del operador ternario para evitar una estructura condicional if-else que en sus dos bloques asigna un valor a la misma celda del vector. Es una pr´actica frecuente y da lugar, una vez acostumbrado, a programas bastante legibles. 3.6.2. Recursi´on indirecta y declaraci´on anticipada C debe conocer la cabecera de una funci´on antes de que sea llamada, es decir, debe conocer el tipo de retorno y el n´umero y tipo de sus par´ametros. Normalmente ello no plantea ning´un problema: basta con definir la funci´on antes de su uso, pero no siempre es posible. Imagina que una funci´on f necesita llamar a una funci´on g y que g, a su vez, necesita llamar a f (recursi´on indirecta). ¿Cu´al ponemos delante? La soluci´on es f´acil: da igual, la que quieras, pero debes hacer una declaraci´on anticipada de la funci´on que defines en segundo lugar. La declaraci´on anticipada no incluye el cuerpo de la funci´on: consiste en la declaraci´on del tipo de retorno, identificador de funci´on y lista de par´ametros con su tipo, es decir, es un prototipo o perfil de la funci´on en cuesti´on. Estudia este ejemplo6 : 6El ejemplo es meramente ilustrativo: hay formas mucho m´as eficientes de saber si un n´umero es par o impar. Introducci´on a la Programaci´on con C 197
  • 204. 3.7 Macros 1 int impar(int a); 2 3 int par(int a) 4 { 5 if (a==0) 6 return 1; 7 else 8 return (impar(a-1)); 9 } 10 11 int impar(int a) 12 { 13 if (a==0) 14 return 0; 15 else 16 return (par(a-1)); 17 } La primera l´ınea es una declaraci´on anticipada de la funci´on impar, pues se usa antes de haber sido definida. Con la declaraci´on anticipada hemos ((adelantado)) la informaci´on acerca de qu´e tipo de valores aceptar´a y devolver´a la funci´on. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 211 Dibuja el estado de la pila cuando se llega al caso base en la llamada recursiva impar(7). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La declaraci´on anticipada resulta necesaria para programas con recursi´on indirecta, pero tambi´en la encontrar´as (o usar´as) en programas sin recursi´on. A veces conviene definir funciones en un orden que facilite la lectura del programa, y es f´acil que se defina una funci´on despu´es de su primer uso. Pongamos por caso el programa ordena.c en el que hemos implementado el m´etodo de ordenaci´on por fusi´on: puede que resulte m´as legible definir primero mergesort y despu´es merge pues, a fin de cuentas, las hemos desarrollado en ese orden. De definirlas as´ı, necesitar´ıamos declarar anticipadamente merge: ordena.c 1 #include <stdio.h> 2 3 #define TALLA 100 4 5 void merge(int v[], int inicio1, int final1, int final2); // Declaraci´on anticipada. 6 7 void mergesort(int v[], int inicio, int final) 8 { 9 if (final - inicio == 0) 10 return; 11 else { 12 mergesort ( v, inicio, (inicio+final) / 2 ); 13 mergesort ( v, (inicio+final) / 2 + 1, final ); 14 merge( v, inicio, (inicio+final) / 2, final ); // Podemos usarla: se ha declarado antes. 15 } 16 } 17 18 void merge(int v[], int inicio1, int final1, int final2) // Y ahora se define. 19 { 20 ... 3.7. Macros El preprocesador permite definir un tipo especial de funciones que, en el fondo, no lo son: las macros. Una macro tiene par´ametros y se usa como una funci´on cualquiera, pero las llamadas no se traducen en verdaderas llamadas a funci´on. Ahora ver´as por qu´e. Vamos con un ejemplo: 198 Introducci´on a la Programaci´on con C
  • 205. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones Prototipo por defecto y declaraci´on anticipada Si usas una funci´on antes de definirla y no has preparado una declaraci´on anticipada, C deduce el tipo de cada par´ametro a partir de la forma en la que se le invoca. Este truco funciona a veces, pero es frecuente que sea fuente de problemas. Considera este ejemplo: 1 int f(int y) 2 { 3 return 1 + g(y); 4 } 5 6 float g(float x) 7 { 8 return x*x; 9 } En la l´ınea 3 se usa g y a´un no se ha definido. Por la forma de uso, el compilador deduce que su perfile es int g(int x). Pero, al ver la definici´on, detecta un conflicto. El problema se soluciona alterando el orden de definici´on de las funciones o, si se prefiere, mediante una declaraci´on anticipada: 1 float g(float x); 2 3 int f(int y) 4 { 5 return 1 + g(y); 6 } 7 8 float g(float x) 9 { 10 return x*x; 11 } 1 #define CUADRADO(x) x*x La directiva con la que se define una macro es #define, la misma con la que declar´abamos constantes. La diferencia est´a en que la macro lleva uno o m´as par´ametros (separados por comas) encerrados entre par´entesis. Este programa define y usa la macro CUADRADO: 1 #include <stdio.h> 2 3 #define CUADRADO(x) x*x 4 5 int main (void) 6 { 7 printf ("El cuadrado de %d es %dn", 2, CUADRADO(2)); 8 return 0; 9 } El compilador no llega a ver nunca la llamada a CUADRADO. La raz´on es que el preprocesador la sustituye por su cuerpo, consiguiendo que el compilador vea esta otra versi´on del programa: 1 #include <stdio.h> 2 3 4 5 int main (void) 6 { 7 printf ("El cuadrado de %d es %dn", 2, 2*2); 8 return 0; 9 } Las macros presentan algunas ventajas frente a las funciones: Introducci´on a la Programaci´on con C 199
  • 206. 3.7 Macros Por regla general, son m´as r´apidas que las funciones, pues al no implicar una llamada a funci´on en tiempo de ejecuci´on nos ahorramos la copia de argumentos en pila y el salto/retorno a otro lugar del programa. No obligan a dar informaci´on de tipo acerca de los par´ametros ni del valor de retorno. Por ejemplo, esta macro devuelve el m´aximo de dos n´umeros, sin importar que sean enteros o flotantes: 1 #define MAXIMO(A, B) ((A > B) ? A : B) Pero tienen serios inconvenientes: La definici´on de la macro debe ocupar, en principio, una sola l´ınea. Si ocupa m´as de una l´ınea, hemos de finalizar todas menos la ´ultima con el car´acter (()) justo antes del salto de l´ınea. Inc´omodo. No puedes definir variables locales.7 No admiten recursi´on. Son peligros´ısimas. ¿Qu´e crees que muestra por pantalla este programa?: 1 #include <stdio.h> 2 3 #define CUADRADO(x) x*x 4 5 int main (void) 6 { 7 printf ("El cuadrado de 6 es %dn", CUADRADO(3+3)); 8 return 0; 9 } ¿36?, es decir, ¿el cuadrado de 6? Pues no es eso lo que obtienes, sino 15. ¿Por qu´e? El preprocesador sustituye el fragmento CUADRADO(3+3) por. . . ¡3+3*3+3! El resultado es, efectivamente, 15, y no el que esper´abamos. Puedes evitar este problema usando par´entesis: 1 #include <stdio.h> 2 3 #define CUADRADO(x) (x)*(x) 4 5 main (void) 6 { 7 printf ("El cuadrado de 6 es %dn", CUADRADO(3+3)); 8 return 0; 9 } Ahora el fragmento CUADRADO(3+3) se sustituye por (3+3)*(3+3), que es lo que espera- mos. Otro problema resuelto. No te f´ıes. Ya te hemos dicho que las macros son peligrosas. Sigue estando mal. ¿Qu´e esperas que calcule 1.0/CUADRADO(3+3)?, ¿el valor de 1/36, es decir, 0.02777. . . ? Te equi- vocas. La expresi´on 1.0/CUADRADO(3+3) se convierte en 1.0/(3+3)*(3+3), que es 1/6 · 6, o sea, 1, no 1/36. La soluci´on pasa por a˜nadir nuevos par´entesis: 1 #include <stdio.h> 2 3 #define CUADRADO(x) ((x)*(x)) 4 5 ... 7No del todo cierto, pero no entraremos en detalles. 200 Introducci´on a la Programaci´on con C
  • 207. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones ¿Ahora s´ı? La expresi´on 1.0/CUADRADO(3+3) se convierte en 1.0/((3+3)*(3+3)), que es 1/36. Pero todav´ıa hay un problema: si ejecutamos este fragmento de c´odigo: 1 i = 3; 2 z = CUADRADO(i++); la variable se incrementa 2 veces, y no una s´ola. Ten en cuenta que el compilador traduce lo que ((ve)), y ((ve)) esto: 1 i = 3; 2 z = ((i++)*(i++)); Y este problema no se puede solucionar. ¡Recuerda! Si usas macros, toda precauci´on es poca. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 212 Dise˜na una macro que calcule la tangente de una cantidad de radianes. Puedes usar las funciones sin y cos de math.h, pero ninguna otra. · 213 Dise˜na una macro que devuelva el m´ınimo de dos n´umeros, sin importar si son enteros o flotantes. · 214 Dise˜na una macro que calcule el valor absoluto de un n´umero, sin importar si es entero o flotante. · 215 Dise˜na una macro que decremente una variable entera si y s´olo si es positiva. La macro devolver´a el valor ya decrementado o inalterado, seg´un convenga. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.8. Otras cuestiones acerca de las funciones 3.8.1. Funciones inline Los inconvenientes de las macros desaconsejan su uso. Lenguajes como C++ dan soporte a las macros s´olo por compatibilidad con C, pero ofrecen alternativas mejores. Por ejemplo, puedes definir funciones inline. Una funci´on inline es como cualquier otra funci´on, s´olo que las llamadas a ella se gestionan como las llamadas a macros: se sustituye la llamada por el c´odigo que se ejecutar´ıa en ese caso, o sea, por el cuerpo de la funci´on con los valores que se suministren para los par´ametros. Las funciones inline presentan muchas ventajas frente a la macros. Entre ellas, la posibilidad de utilizar variables locales o la no necesidad de utilizar par´entesis alrededor de toda aparici´on de un par´ametro. Las funciones inline son tan ´utiles que compiladores como gcc las integran desde hace a˜nos como extensi´on propia del lenguaje C y han pasado a formar parte del lenguaje C99. Al compilar un programa C99 como ´este: 1 #include <stdio.h> 2 3 inline int doble(int a) 4 { 5 return a * 2; 6 } 7 8 int main(void) 9 { 10 int i; 11 12 for (i=0; i<10; i++) 13 printf ("%dn", doble(i+1) ); 14 15 return 0; 16 } no se genera c´odigo de m´aquina con 10 llamadas a la funci´on doble. El c´odigo de m´aquina que se genera es virtualmente id´entico al que se genera para este otro programa equivalente: Introducci´on a la Programaci´on con C 201
  • 208. 3.8 Otras cuestiones acerca de las funciones 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int i; 6 7 for (i=0; i<10; i++) 8 printf ("%dn", ((i+1) * 2) ); 9 10 return 0; 11 } Hay ocasiones, no obstante, en las que el compilador no puede efectuar la sustituci´on de la llamada a funci´on por su cuerpo. Si la funci´on es recursiva, por ejemplo, la sustituci´on es imposible. Pero aunque no sea recursiva, el compilador puede juzgar que una funci´on es excesivamente larga o compleja para que compense efectuar la sustituci´on. Cuando se declara una funci´on como inline, s´olo se est´a sugiriendo al compilador que efect´ue la sustituci´on, pero ´este tiene la ´ultima palabra sobre si habr´a o no una verdadera llamada a funci´on. 3.8.2. Variables locales static Hay un tipo especial de variable local: las variables static. Una variable static es invisible fuera de la funci´on, como cualquier otra variable local, pero recuerda su valor entre diferentes ejecuciones de la funci´on en la que se declara. Veamos un ejemplo: 1 #include <stdio.h> 2 3 int turno(void) 4 { 5 static int contador = 0; 6 7 return contador++; 8 } 9 10 int main(void) 11 { 12 int i; 13 14 for (i=0; i<10; i++) 15 printf ("%dn", turno()); 16 return 0; 17 } Si ejecutas el programa aparecer´an por pantalla los n´umeros del 0 al 9. Con cada llamada, contador devuelve su valor y se incrementa en una unidad, sin olvidar su valor entre llamada y llamada. La inicializaci´on de las variables static es opcional: el compilador asegura que empiezan valiendo 0. Vamos a volver a escribir el programa que presentamos en el ejercicio 169 para generar n´umeros primos consecutivos. Esta vez, vamos a hacerlo sin usar una variable global que re- cuerde el valor del ´ultimo primo generado. Usaremos en su lugar una variable local static: primos.c primos.c 1 #include <stdio.h> 2 3 int siguienteprimo(void) 4 { 5 static int ultimoprimo = 0; 6 int esprimo; 7 int i; 8 202 Introducci´on a la Programaci´on con C
  • 209. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones 9 do { 10 ultimoprimo++; 11 esprimo = 1; 12 for (i=2; i<ultimoprimo/2; i++) 13 if (ultimoprimo % i == 0) { 14 esprimo = 0; 15 break; 16 } 17 } while (!esprimo); 18 return ultimoprimo; 19 } 20 21 int main(void) 22 { 23 int i; 24 25 printf ("Los 10 primeros n´umeros primosn"); 26 for (i=0; i<10; i++) 27 printf ("%dn", siguienteprimo()); 28 return 0; 29 } Mucho mejor. Si puedes evitar el uso de variables globales, ev´ıtalo. Las variables locales static pueden ser la soluci´on en bastantes casos. 3.8.3. Paso de funciones como par´ametros Hay un tipo de par´ametro especial que puedes pasar a una funci´on: ¡otra funci´on! Veamos un ejemplo. En este fragmento de programa se definen sendas funciones C que aproximan num´ericamente la integral definida en un intervalo para las funciones matem´aticas f(x) = x2 y f(x) = x3 , respectivamente: 1 float integra_cuadrado (float a, float b, int n) 2 { 3 int i; 4 float s, x; 5 6 s = 0.0; 7 x = a; 8 for (i=0; i<n; i++) { 9 s += x*x * (b-a)/n; 10 x += (b-a)/n; 11 } 12 return s; 13 } 14 15 float integra_cubo (float a, float b, int n) 16 { 17 int i; 18 float s, x; 19 20 s = 0.0; 21 x = a; 22 for (i=0; i<n; i++) { 23 s += x*x*x * (b-a)/n; 24 x += (b-a)/n; 25 } 26 return s; 27 } Las dos funciones que hemos definido son b´asicamente iguales. S´olo difieren en su identificador y en la funci´on matem´atica que integran. ¿No ser´ıa mejor disponer de una ´unica funci´on C, digamos integra, a la que suministremos como par´ametro la funci´on matem´atica que queremos integrar? C lo permite: Introducci´on a la Programaci´on con C 203
  • 210. 3.8 Otras cuestiones acerca de las funciones 1 float integra(float a, float b, int n, float (*f)(float)) 2 { 3 int i; 4 float s, x; 5 6 s = 0.0; 7 x = a; 8 for (i=0; i<n; i++) { 9 s += f(x) * (b-a)/n; 10 x += (b-a)/n; 11 } 12 return s; 13 } Hemos declarado un cuarto par´ametro que es de tipo puntero a funci´on. Cuando llamamos a integra, el cuarto par´ametro puede ser el identificador de una funci´on que reciba un float y devuelva un float: integra.c integra.c 1 #include <stdio.h> 2 3 float integra(float a, float b, int n, float (*f)(float)) 4 { 5 int i; 6 float s, x; 7 8 s = 0.0; 9 x = a; 10 for (i=0; i<n; i++) { 11 s += f(x) * (b-a)/n; 12 x += (b-a)/n; 13 } 14 return s; 15 } 16 17 float cuadrado(float x) 18 { 19 return x*x; 20 } 21 22 float cubo(float x) 23 { 24 return x*x*x; 25 } 26 27 int main(void) 28 { 29 printf ("Integral 1: %fn", integra(0.0, 1.0, 10, cuadrado )); 30 printf ("Integral 2: %fn", integra(0.0, 1.0, 10, cubo )); 31 return 0; 32 } La forma en que se declara un par´ametro del tipo ((puntero a funci´on)) resulta un tanto complicada. En nuestro caso, lo hemos declarado as´ı: float (*f)(float). El primer float indica que la funci´on devuelve un valor de ese tipo. El (*f) indica que el par´ametro f es un puntero a funci´on. Y el float entre par´entesis indica que la funci´on trabaja con un par´ametro de tipo float. Si hubi´esemos necesitado trabajar con una funci´on que recibe un float y un int, hubi´esemos escrito float (*f)(float, int) en la declaraci´on del par´ametro. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 216 ¿Puedes usar la funci´on integra para calcular la integral definida de la funci´on ma- tem´atica sin(x)? ¿C´omo? 204 Introducci´on a la Programaci´on con C
  • 211. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones · 217 Dise˜na una funci´on C capaz de calcular b i=a f(i), siendo f una funci´on matem´atica cualquiera que recibe un entero y devuelve un entero. · 218 Dise˜na una funci´on C capaz de calcular b i=a d j=c f(i, j), siendo f una funci´on matem´atica cualquiera que recibe dos enteros y devuelve un entero. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.9. M´odulos, bibliotecas y unidades de compilaci´on Cuando te enfrentas a la escritura de un programa largo, individualmente o en equipo, te resultar´a virtualmente imposible escribirlo en un ´unico fichero de texto. Resulta m´as pr´actico agrupar diferentes partes del programa en ficheros independientes. Cada fichero puede, por ejemplo, agrupar las funciones, registros y constantes propias de cierto tipo de c´alculos. Proceder as´ı tiene varias ventajas: Mejora la legibilidad del c´odigo (cada fichero es relativamente breve y agrupa tem´aticamente las funciones, registros y constantes). La compilaci´on es m´as r´apida (cuando se modifica un fichero, s´olo es necesario compilar ese fichero). Y, quiz´a lo m´as importante, permite reutilizar c´odigo. Es un beneficio a medio y largo pla- zo. Si, por ejemplo, te dedicas a programar videojuegos tridimensionales, ver´as que todos ellos comparten ciertas constantes, registros y funciones definidas por t´ı o por otros pro- gramadores: tipos de datos para modelar puntos, pol´ıgonos, texturas, etc´etera; funciones que los manipulan, visualizan, leen/escriben en disco, etc´etera. Puedes definir estos ele- mentos en un fichero y utilizarlo en cuantos programas desees. Alternativamente, podr´ıas copiar-y-pegar las funciones, constantes y registros que uno necesita en cada programa, pero no es conveniente en absoluto: corregir un error en una funci´on obligar´ıa a editar todos los programas en los que se peg´o; por contra, si est´a en un solo fichero, basta con corregir la definici´on una sola vez. C permite escribir un programa como una colecci´on de unidades de compilaci´on. El con- cepto es similar al de los m´odulos Python: cada unidad agrupa definiciones de variables, tipos, constantes y funciones orientados a resolver cierto tipo de problemas. Puedes compilar indepen- dientemente cada unidad de compilaci´on (de ah´ı el nombre) de modo que el compilador genere un fichero binario para cada una de ellas. El enlazador se encarga de unir en una ´ultima etapa todas las unidades compiladas para crear un ´unico fichero ejecutable. Lo mejor ser´a que aprendamos sobre unidades de compilaci´on escribiendo una muy sencilla: un m´odulo en el que se define una funci´on que calcula el m´aximo de dos n´umero enteros. El fichero que corresponde a esta unidad de compilaci´on se llamar´a extremos.c. He aqu´ı su contenido: extremos.c extremos.c 1 int maximo(int a, int b) 2 { 3 if (a > b) 4 return a; 5 else 6 return b; 7 } El programa principal se escribir´a en otro fichero llamado principal.c. Dicho programa lla- mar´a a la funci´on maximo: Introducci´on a la Programaci´on con C 205
  • 212. 3.9 M´odulos, bibliotecas y unidades de compilaci´on principal 1.c E principal.c E 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int x, y; 6 7 printf ("Dame un n´umero: "); 8 scanf ("%d", &x); 9 printf ("Dame otro: "); 10 scanf ("%d", &y); 11 printf ("El m´aximo es %dn", maximo(x, y)); 12 return 0; 13 } Hemos marcado el programa como incorrecto. ¿Por qu´e? Ver´as, estamos usando una funci´on, maximo, que no est´a definida en el fichero principal.c. ¿C´omo sabe el compilador cu´antos par´ametros recibe dicha funci´on?, ¿y el tipo de cada par´ametro?, ¿y el tipo del valor de retorno? El compilador se ve obligado a generar c´odigo de m´aquina para llamar a una funci´on de la que no sabe nada. Mala cosa. ¿C´omo se resuelve el problema? Puedes declarar la funci´on sin definirla, es decir, puedes declarar el aspecto de su cabecera (lo que denominamos su prototipo) e indicar que es una funci´on definida externamente: principal 2.c principal.c 1 #include <stdio.h> 2 3 extern int maximo(int a, int b); 4 5 int main(void) 6 { 7 int x, y; 8 9 printf ("Dame un n´umero: "); 10 scanf ("%d", &x); 11 printf ("Dame otro: "); 12 scanf ("%d", &y); 13 printf ("El m´aximo es %dn", maximo(x, y)); 14 return 0; 15 } El prototipo contiene toda la informaci´on ´util para efectuar la llamada a la funci´on, pero no contiene su cuerpo: la cabecera acaba con un punto y coma. F´ıjate en que la declaraci´on del prototipo de la funci´on maximo empieza con la palabra clave extern. Con ella se indica al compilador que maximo est´a definida en alg´un m´odulo ((externo)). Tambi´en puedes indicar con extern que una variable se define en otro m´odulo. Puedes compilar el programa as´ı: $ gcc extremos.c -c $ gcc principal.c -c $ gcc principal.o extremos.o -o principal La compilaci´on necesita tres pasos: uno por cada unidad de compilaci´on y otro para enlazar. 1. El primer paso (gcc extremos.c -c) traduce a c´odigo de m´aquina el fichero o unidad de compilaci´on extremos.c. La opci´on -c indica al compilador que extremos.c es un m´odulo y no define a la funci´on main. El resultado de la compilaci´on se deja en un fichero llamado extremos.o. La extensi´on ((.o)) abrevia el t´ermino ((object code)), es decir, ((c´odigo objeto)). Los ficheros con extensi´on ((.o)) contienen el c´odigo de m´aquina de nuestras funciones8 , pero no es directamente ejecutable. 2. El segundo paso (gcc principal.c -c) es similar al primero y genera el fichero principal.o a partir de principal.c. 8. . . pero no s´olo eso: tambi´en contienen otra informaci´on, como la denominada tabla de s´ımbolos. 206 Introducci´on a la Programaci´on con C
  • 213. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones 3. El tercer paso (gcc principal.o extremos.o -o principal) es especial. El compilador recibe dos ficheros con extensi´on ((.o)) y genera un ´unico fichero ejecutable, llamado prin- cipal. Este ´ultimo paso se encarga de enlazar las dos unidades compiladas para generar el fichero ejecutable. Por enlazar entendemos que las llamadas a funciones cuyo c´odigo de m´aquina era desco- nocido (estaba en otra unidad de compilaci´on) se traduzcan en ((saltos)) a las direcciones en las que se encuentran los subprogramas de c´odigo m´aquina correspondientes (y que ahora se conocen). Aqu´ı tienes un diagrama que ilustra el proceso: Enlazador principal Paso 3 extremos.c Compilador extremos.o Paso 1 principal.c Compilador principal.o Paso 2 Puedes ahorrarte un paso fundiendo los dos ´ultimos en uno s´olo. As´ı: $ gcc extremos.c -c $ gcc principal.c extremos.o -o principal Este diagrama muestra todos los pasos del proceso a los que aludimos: principal.c Compilador Enlazador principal Paso 2 extremos.c Compilador extremos.o Paso 1 Para conseguir un programa ejecutable es necesario que uno de los m´odulos (¡pero s´olo uno de ellos!) defina una funci´on main. Si ning´un m´odulo define main o si main se define en m´as de un m´odulo, el enlazador protestar´a y no generar´a fichero ejecutable alguno. 3.9.1. Declaraci´on de prototipos en cabeceras Hemos resuelto el problema de gestionar diferentes unidades de compilaci´on, pero la soluci´on de tener que declarar el prototipo de cada funci´on en toda unidad de compilaci´on que la usa no es muy buena. Hay una mejor: definir un fichero de cabecera. Los ficheros de cabecera agrupan las declaraciones de funciones (y cualquier otro elemento) definidos en un m´odulo. Las cabeceras son ficheros con extensi´on ((.h)) (es un convenio: la ((h)) es abreviatura de ((header))). Nuestra cabecera ser´a este fichero: extremos.h extremos.h 1 extern int maximo(int a, int b); Para incluir la cabecera en nuestro programa, escribiremos una nueva directiva #include: principal.c principal.c 1 #include <stdio.h> 2 #include "extremos.h" 3 4 int main(void) 5 { 6 int x, y; 7 Introducci´on a la Programaci´on con C 207
  • 214. 3.9 M´odulos, bibliotecas y unidades de compilaci´on Documentaci´on y cabeceras Es importante que documentes bien los ficheros de cabecera, pues es frecuente que los programadores que usen tu m´odulo lo consulten para hacerse una idea de qu´e ofrece. Nuestro m´odulo podr´ıa haberse documentado as´ı: extremos.h 1 /******************************************************* 2 * M´odulo: extremos 3 * 4 * Prop´osito: funciones para c´alculo de valores m´aximos 5 * y m´ınimos. 6 * 7 * Autor: A. U. Thor. 8 * 9 * Fecha: 12 de enero de 1997 10 * 11 * Estado: Incompleto. Falta la funci´on minimo. 12 *******************************************************/ 14 15 extern int maximo(int a, int b); 16 /* Calcula el m´aximo de dos n´umero enteros a y b. */ ¿Y por qu´e los programadores no miran directamente el fichero .c en lugar del .h cuando quieren consultar algo? Por varias razones. Una de ellas es que, posiblemente, el .c no est´e accesible. Si el m´odulo es un producto comercial, probablemente s´olo les hayan vendido el m´odulo ya compilado (el fichero .o) y el fichero de cabecera. Pero incluso si se tiene acceso al .c, puede ser preferible ver el .h. El fichero .c puede estar plagado de detalles de implementaci´on, funciones auxiliares, variables para uso interno, etc., que hacen engorrosa su lectura. El fichero de cabecera contiene una somera declaraci´on de cada uno de los elementos del m´odulo que se ((publican)) para su uso en otros m´odulos o programas, as´ı que es una especie de resumen del .c. 8 printf ("Dame un n´umero: "); 9 scanf ("%d", &x); 10 printf ("Dame otro: "); 11 scanf ("%d", &y); 12 printf ("El m´aximo es %dn", maximo(x, y)); 13 return 0; 14 } La ´unica diferencia con respecto a otros #include que ya hemos usado estriba en el uso de comillas dobles para encerrar el nombre del fichero, en lugar de los caracteres ((<)) y ((>)). Con ello indicamos al preprocesador que el fichero extremos.h se encuentra en nuestro directorio activo. El preprocesador se limita a sustituir la l´ınea en la que aparece #include "extremos.h" por el contenido del fichero. En un ejemplo tan sencillo no hemos ganado mucho, pero si el m´odulo extremos.o contuviera muchas funciones, con s´olo una l´ınea habr´ıamos conseguido ((importarlas)) todas. Aqu´ı tienes una actualizaci´on del gr´afico que muestra el proceso completo de compilaci´on: principal.c Preprocesador Compilador Enlazador principal extremos.c Compilador extremos.o extremos.h 208 Introducci´on a la Programaci´on con C
  • 215. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones Bibliotecas Ya has usado funciones y datos predefinidos, como las funciones y las constantes ma- tem´aticas. Hemos hablado entonces del uso de la biblioteca matem´atica. ¿Por qu´e ((biblioteca)) y no ((m´odulo))? Una biblioteca es m´as que un m´odulo: es un conjunto de m´odulos. Cuando se tiene una pl´eyade de ficheros con extensi´on ((.o)), conviene empaquetarlos en uno solo con extensi´on ((.a)) (por ((archive))). Los ficheros con extensi´on ((.a)) son similares a los ficheros con extensi´on ((.tar)): meras colecciones de ficheros. De hecho, ((tar)) (tape archiver) es una evoluci´on de ((.ar)) (por ((archiver))), el programa con el que se manipulan los ficheros con extensi´on ((.a)). La biblioteca matem´atica, por ejemplo, agrupa un mont´on de m´odulos. En un sistema Linux se encuentra en el fichero /usr/lib/libm.a y puedes consultar su contenido con esta orden: $ ar tvf /usr/lib/libm.a rw-r--r-- 0/0 29212 Sep 9 18:17 2002 k_standard.o rw-r--r-- 0/0 8968 Sep 9 18:17 2002 s_lib_version.o rw-r--r-- 0/0 9360 Sep 9 18:17 2002 s_matherr.o rw-r--r-- 0/0 8940 Sep 9 18:17 2002 s_signgam.o ... rw-r--r-- 0/0 1152 Sep 9 18:17 2002 slowexp.o rw-r--r-- 0/0 1152 Sep 9 18:17 2002 slowpow.o Como puedes ver, hay varios ficheros con extensi´on ((.o)) en su interior. (S´olo te mostra- mos el principio y el final del resultado de la llamada, pues hay un total de ¡395 ficheros!) Cuando usas la biblioteca matem´atica compilas as´ı: $ gcc programa.c -lm -o programa o, equivalentemente, as´ı: $ gcc programa.c /usr/lib/libm.a -o programa En el segundo caso hacemos expl´ıcito el nombre de la biblioteca en la que se encuentran las funciones matem´aticas. El enlazador no s´olo sabe tratar ficheros con extensi´on ((.o)): tambi´en sabe buscarlos en los de extensi´on ((.a)). En cualquier caso, sigue siendo necesario que las unidades de compilaci´on conozcan el perfil de las funciones que usan y est´an definidas en otros m´odulos o bibliotecas. Por eso inclu´ımos, cuando conviene, el fichero math.h en nuestros programas. Hay infinidad de bibliotecas que agrupan m´odulos con utilidades para diferentes campos de aplicaci´on: resoluci´on de problemas matem´aticos, dise˜no de videojuegos, reproducci´on de m´usica, etc. Algunas son c´odigo abierto, en cuyo caso se distribuyen con los ficheros de extensi´on ((.c)), los ficheros de extensi´on ((.h)) y alguna utilidad para facilitar la compilaci´on (un makefile). Cuando son comerciales es frecuente que se mantenga el c´odigo fuente en privado. En tal caso, se distribuye el fichero con extensi´on ((.a)) (o una colecci´on de ficheros con extensi´on ((.o))) y uno o m´as ficheros con extensi´on ((.h)). 3.9.2. Declaraci´on de variables en cabeceras No s´olo puedes declarar funciones en los ficheros de cabecera. Tambi´en puedes definir constantes, variables y registros. Poco hay que decir sobre las constantes. Basta con que las definas con #define en el fichero de cabecera. Las variables, sin embargo, s´ı plantean un problema. Este m´odulo, por ejemplo, declara una variable entera en mimodulo.c: mimodulo.c 1 int variable; Si deseamos que otras unidades de compilaci´on puedan acceder a esa variable, tendremos que incluir su declaraci´on en la cabecera. ¿C´omo? Una primera idea es poner, directamente, la declaraci´on as´ı: E mimodulo.h E 1 int variable; Introducci´on a la Programaci´on con C 209
  • 216. 3.9 M´odulos, bibliotecas y unidades de compilaci´on Pero es incorrecta. El problema radica en que cuando incluyamos la cabecera mimodulo.h en nuestro programa, se insertar´a la l´ınea int variable;, sin m´as, as´ı que se estar´a definiendo una nueva variable con el mismo identificador que otra. Y declarar dos variables con el mismo identificador es un error. Quien detecta el error es el enlazador: cuando vaya a generar el programa ejecutable, encon- trar´a que hay dos objetos que tienen el mismo identificador, y eso est´a prohibido. La soluci´on es sencilla: preceder la declaraci´on de variable en la cabecera mimodulo.h con la palabra reservada extern: mimodulo.h 1 extern int variable; De ese modo, cuando se compila un programa que incluye a mimodulo.h, el compilador sabe que variable es de tipo int y que est´a definida en alguna unidad de compilaci´on, por lo que no la crea por segunda vez. 3.9.3. Declaraci´on de registros en cabeceras Finalmente, puedes declarar tambi´en registros en las cabeceras. Como los programas que cons- truiremos son sencillos, no se plantear´a problema alguno con la definici´on de registros: basta con que pongas su declaraci´on en la cabecera, sin m´as. Pero si tu programa incluye dos cabeceras que, a su vez, incluyen ambas a una tercera donde se definen constantes o registros, puedes tener problemas. Un ejemplo ilustrar´a mejor el tipo de dificultades al que nos enfrentamos. Supongamos que un fichero a.h define un registro: a.h 1 // Cabecera a.h 2 struct A { 3 int a; 4 }; 5 // Fin de cabecera a.h Ahora, los ficheros b.h y c.h incluyen a a.h y declaran la existencia de sendas funciones: b.h 1 // Cabecera b.h 2 #include "a.h" 3 4 int funcion_de_b_punto_h(int x); 5 // Fin de cabecera b.h c.h 1 // Cabecera c.h 2 #include "a.h" 3 4 int funcion_de_c_punto_h(int x); 5 // Fin de cabecera c.h Y, finalmente, nuestro programa incluye tanto a b.h como a c.h: programa.c 1 #include <stdio.h> 2 3 #include "b.h" 4 5 #include "c.h" 6 7 int main(void) 8 { 9 ... 10 } El resultado es que el a.h acaba quedando incluido ¡dos veces! Tras el paso de programa.c por el preprocesador, el compilador se enfrenta, a este texto: 210 Introducci´on a la Programaci´on con C
  • 217. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 3 Funciones programa.c 1 #include <stdio.h> 2 3 // Cabecera b.h. 4 // Cabecera a.h. 5 struct A { 6 int a; 7 }; 8 // Fin de cabecera a.h. 9 10 int funcion_de_b_punto_h(int x); 11 // Fin de cabecera b.h. 12 13 // Cabecera c.h. 14 // Cabecera a.h. 15 struct A { 16 int a; 17 }; 18 // Fin de cabecera a.h. 19 20 int funcion_de_c_punto_h(int x); 21 // Fin de cabecera c.h. 22 23 int main(void) 24 { 25 ... 26 } El compilador encuentra, por tanto, la definici´on de struct A por duplicado, y nos avisa del ((error)). No importa que las dos veces se declare de la misma forma: C lo considera ilegal. El problema puede resolverse reescribiendo a.h (y, en general, cualquier fichero cabecera) as´ı: 1 // Cabecera de a.h 2 #ifndef A_H 3 #define A_H 4 5 struct A { 6 int a; 7 }; 8 9 #endif 10 // Fin de cabecera de a.h Las directivas #ifndef/#endif marcan una zona de ((c´odigo condicional)). Se interpretan as´ı: ((si la constante A_H no est´a definida, entonces incluye el fragmento hasta el #endif, en caso contrario, s´altate el texto hasta el #endif)). O sea, el compilador ver´a o no lo que hay entre las l´ıneas 3 y 8 en funci´on de si existe o no una determinada constante. No debes confundir estas directivas con una sentencia if: no lo son. La sentencia if permite ejecutar o no un bloque de sentencias en funci´on de que se cumpla o no una condici´on en tiempo de ejecuci´on. Las directivas presentadas permiten que el compilador vea o no un fragmento arbitrario de texto en funci´on de si existe o no una constante en tiempo de compilaci´on. Observa que lo primero que se hace en ese fragmento de programa es definir la constante A_H (l´ınea 3). La primera vez que se incluya la cabecera a.h no estar´a a´un definida A_H, as´ı que se incluir´an las l´ıneas 3–8. Uno de los efectos ser´a que A_H pasar´a a estar definida. La segunda vez que se incluya la cabecera a.h, A_H ya estar´a definida, as´ı que el compilador no ver´a por segunda vez la definici´on de struct A. El efecto final es que la definici´on de struct A s´olo se ve una vez. He aqu´ı lo que resulta de programa.c tras su paso por el preprocesador: programa.c 1 #include <stdio.h> 2 3 // Cabecera b.h. 4 // Cabecera a.h. Introducci´on a la Programaci´on con C 211
  • 218. 3.9 M´odulos, bibliotecas y unidades de compilaci´on 5 struct A { 6 int a; 7 }; 8 // Fin de cabecera a.h. 9 10 int funcion_de_b_punto_h(int x); 11 // Fin de cabecera b.h. 12 13 // Cabecera c.h. 14 // Cabecera a.h. 15 // Fin de cabecera a.h. 16 17 int funcion_de_c_punto_h(int x); 18 // Fin de cabecera c.h. 19 20 int main(void) 21 { 22 ... 23 } La segunda inclusi´on de a.h no ha supuesto el copiado del texto guardado entre directivas #ifndef/#endif. Ingenioso, ¿no? 212 Introducci´on a la Programaci´on con C
  • 219. Cap´ıtulo 4 Estructuras de datos: memoria din´amica La Reina se puso congestionada de furia, y, tras lanzarle una mirada felina, empez´o a gritar: ((¡Que le corten la cabeza! ¡Que le corten. . . !)). Lewis Carroll, Alicia en el Pa´ıs de las Maravillas. Vimos en el cap´ıtulo 2 que los vectores de C presentaban un serio inconveniente con respecto a las listas de Python: su tama˜no deb´ıa ser fijo y conocido en tiempo de compilaci´on, es decir, no pod´ıamos alargar o acortar los vectores para que se adaptaran al tama˜no de una serie de datos durante la ejecuci´on del programa. C permite una gesti´on din´amica de la memoria, es decir, solicitar memoria para albergar el contenido de estructuras de datos cuyo tama˜no exacto no conocemos hasta que se ha iniciado la ejecuci´on del programa. Estudiaremos aqu´ı dos formas de superar las limitaciones de tama˜no que impone el C: mediante vectores cuyo tama˜no se fija en tiempo de ejecuci´on, y mediante registros enlazados, tambi´en conocidos como listas enlazadas (o, simplemente, listas). Ambas aproximaciones se basan en el uso de punteros y cada una de ellas presenta diferentes ventajas e inconvenientes. 4.1. Vectores din´amicos Sabemos definir vectores indicando su tama˜no en tiempo de compilaci´on: 1 #define TALLA 10 2 3 int a[TALLA]; Pero, ¿y si no sabemos a priori cu´antos elementos debe albergar el vector?1 Por lo estudiado hasta el momento, podemos definir TALLA como el n´umero m´as grande de elementos posible, el n´umero de elementos para el peor de los casos. Pero, ¿y si no podemos determinar un n´umero m´aximo de elementos? Aunque pudi´eramos, ¿y si ´este fuera tan grande que, en la pr´actica, supusiera un despilfarro de memoria intolerable para situaciones normales? Imagina una aplicaci´on de agenda telef´onica personal que, por si acaso, reserva 100000 entradas en un vector. Lo m´as probable es que un usuario convencional no gaste m´as de un centenar. Estaremos desperdiciando, pues, unas 99900 celdas del vector, cada una de las cuales puede consistir en un centenar de bytes. Si todas las aplicaciones del ordenador se dise˜naran as´ı, la memoria disponible se agotar´ıa rapid´ısimamente. 1En la secci´on 3.5.3 vimos c´omo definir vectores locales cuya talla se decide al ejecutar una funci´on: lo que denominamos ((vectores de longitud variable)). Nos proponemos dos objetivos: por una parte, poder redimensionar vectores globales; y, por otro, vamos a permitir que un vector crezca y decrezca en tama˜no cuantas veces queramos. Los ((vectores de longitud variable)) que estudiamos en su momento son inapropiados para cualquiera de estos dos objetivos. Introducci´on a la Programaci´on con C 213
  • 220. 4.1 Vectores din´amicos 4.1.1. malloc, free y NULL Afortunadamente, podemos definir, durante la ejecuci´on del programa, vectores cuyo tama˜no es exactamente el que el usuario necesita. Utilizaremos para ello dos funciones de la biblioteca est´andar (disponibles incluyendo la cabecera stdlib.h): malloc (abreviatura de ((memory allocate)), que podemos traducir por ((reservar memo- ria))): solicita un bloque de memoria del tama˜no que se indique (en bytes); free (que en ingl´es significa ((liberar))): libera memoria obtenida con malloc, es decir, la marca como disponible para futuras llamadas a malloc. Para hacernos una idea de c´omo funciona, estudiemos un ejemplo: vector dinamico.c vector dinamico.c 1 #include <stdlib.h> 2 #include <stdio.h> 3 4 int main(void) 5 { 6 int * a; 7 int talla, i; 8 9 printf ("N´umero de elementos: "); scanf ("%d", &talla); 10 a = malloc( talla * sizeof(int) ); 11 for (i=0; i<talla; i++) 12 a[i] = i; 13 free(a); 14 a = NULL; 15 16 return 0; 17 } F´ıjate en c´omo se ha definido el vector a (l´ınea 6): como int * a, es decir, como puntero a entero. No te dejes enga˜nar: no se trata de un puntero a un entero, sino de un puntero a una secuencia de enteros. Ambos conceptos son equivalentes en C, pues ambos son meras direcciones de memoria. La variable a es un vector din´amico de enteros, pues su memoria se obtiene din´amicamente, esto es, en tiempo de ejecuci´on y seg´un convenga a las necesidades. No sabemos a´un cu´antos enteros ser´an apuntados por a, ya que el valor de talla no se conocer´a hasta que se ejecute el programa y se lea por teclado. Sigamos. La l´ınea 10 reserva memoria para talla enteros y guarda en a la direcci´on de memoria en la que empiezan esos enteros. La funci´on malloc presenta un prototipo similar a ´este: stdlib.h ... void * malloc(int bytes); ... Es una funci´on que devuelve un puntero especial, del tipo de datos void *. ¿Qu´e significa void *? Significa ((puntero a cualquier tipo de datos)), o sea, ((direcci´on de memoria)), sin m´as. La funci´on malloc no se usa s´olo para reservar vectores din´amicos de enteros: puedes reservar con ella vectores din´amicos de cualquier tipo base. Analicemos ahora el argumento que pasamos a malloc. La funci´on espera recibir como argumento un n´umero entero: el n´umero de bytes que queremos reservar. Si deseamos reservar talla valores de tipo int, hemos de solicitar memoria para talla * sizeof(int) bytes. Recuerda que sizeof(int) es la ocupaci´on en bytes de un dato de tipo int (y que estamos asumiendo que es de 4). Si el usuario decide que talla valga, por ejemplo, 5, se reservar´a un total de 20 bytes y la memoria quedar´a as´ı tras ejecutar la l´ınea 10: a 0 1 2 3 4 214 Introducci´on a la Programaci´on con C
  • 221. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica Es decir, se reserva suficiente memoria para albergar 5 enteros. Como puedes ver, las l´ıneas 11–12 tratan a a como si fuera un vector de enteros cualquiera. Una vez has reservado memoria para un vector din´amico, no hay diferencia alguna entre ´el y un vector est´atico desde el punto de vista pr´actico. Ambos pueden indexarse (l´ınea 12) o pasarse como argumento a funciones que admiten un vector del mismo tipo base. Aritm´etica de punteros Una curiosidad: el acceso indexado a[0] es equivalente a *a. En general, a[i] es equivalente a *(a+i), es decir, ambas son formas de expresar el concepto ((accede al contenido de la direcci´on a con un desplazamiento de i veces el tama˜no del tipo base)). La sentencia de asignaci´on a[i] = i podr´ıa haberse escrito como *(a+i) = i. En C es posible sumar o restar un valor entero a un puntero. El entero se interpreta como un desplazamiento dado en unidades ((tama˜no del tipo base)) (en el ejemplo, 4 bytes, que es el tama˜no de un int). Es lo que se conoce por aritm´etica de punteros. La aritm´etica de punteros es un punto fuerte de C, aunque tambi´en tiene sus detractores: resulta sencillo provocar accesos incorrectos a memoria si se usa mal. Finalmente, la l´ınea 13 del programa libera la memoria reservada y la l´ınea 14 guarda en a un valor especial: NULL. La funci´on free tiene un prototipo similar a ´este: stdlib.h ... void free(void * puntero); ... Como ves, free recibe un puntero a cualquier tipo de datos: la direcci´on de memoria en la que empieza un bloque previamente obtenido con una llamada a malloc. Lo que hace free es liberar ese bloque de memoria, es decir, considerar que pasa a estar disponible para otras posibles llamadas a malloc. Es como cerrar un fichero: si no necesito un recurso, lo libero para que otros lo puedan aprovechar.2 Puedes aprovechar as´ı la memoria de forma ´optima. Recuerda: tu programa debe efectuar una llamada a free por cada llamada a malloc. Es muy importante. Conviene que despu´es de hacer free asignes al puntero el valor NULL, especialmente si la variable sigue ((viva)) durante bastante tiempo. NULL es una constante definida en stdlib.h. Si un puntero vale NULL, se entiende que no apunta a un bloque de memoria. Gr´aficamente, un puntero que apunta a NULL se representa as´ı: a Liberar memoria no cambia el valor del puntero La llamada a free libera la memoria apuntada por un puntero, pero no modifica el valor de la variable que se le pasa. Imagina que un bloque de memoria de 10 enteros que empieza en la direcci´on 1000 es apuntado por una variable a de tipo int *, es decir, imagina que a vale 1000. Cuando ejecutamos free(a), ese bloque se libera y pasa a estar disponible para eventuales llamadas a malloc, pero ¡a sigue valiendo 1000! ¿Por qu´e? Porque a se ha pasado a free por valor, no por referencia, as´ı que free no tiene forma de modificar el valor de a. Es recomendable que asignes a a el valor NULL despu´es de una llamada a free, pues as´ı haces expl´ıcito que la variable a no apunta a nada. Recuerda, pues, que es responsabilidad tuya y que conviene hacerlo: asigna expl´ıcitamente el valor NULL a todo puntero que no apunte a memoria reservada. La funci´on malloc puede fallar por diferentes motivos. Podemos saber cu´ando ha fallado porque malloc lo notifica devolviendo el valor NULL. Imagina que solicitas 2 megabytes de memoria en un ordenador que s´olo dispone de 1 megabyte. En tal caso, la funci´on malloc devolver´a el valor NULL para indicar que no pudo efectuar la reserva de memoria solicitada. 2Y, como en el caso de un fichero, si no lo liberas t´u expl´ıcitamente, se libera autom´aticamente al finalizar la ejecuci´on del programa. A´un as´ı, te exigimos disciplina: obl´ıgate a liberarlo t´u mismo tan pronto dejes de necesitarlo. Introducci´on a la Programaci´on con C 215
  • 222. 4.1 Vectores din´amicos Los programas correctamente escritos deben comprobar si se pudo obtener la memoria so- licitada y, en caso contrario, tratar el error. 1 a = malloc(talla * sizeof(int)); 2 if (a == NULL) { 3 printf ("Error: no hay memoria suficienten"); 4 } 5 else { 6 ... 7 } Es posible (y una forma de expresi´on idiom´atica de C) solicitar la memoria y comprobar si se pudo obtener en una ´unica l´ınea (presta atenci´on al uso de par´entesis, es importante): 1 if ( (a = malloc(talla * sizeof(int))) == NULL) { 2 printf ("Error: no hay memoria suficienten"); 3 } 4 else { 5 ... 6 } Nuestros programas, sin embargo, no incluir´an esta comprobaci´on. Estamos aprendiendo a pro- gramar y sacrificaremos las comprobaciones como ´esta en aras de la legibilidad de los programas. Pero no lo olvides: los programas con un acabado profesional deben comprobar y tratar posibles excepciones, como la no existencia de suficiente memoria. Fragmentaci´on de la memoria Ya hemos dicho que malloc puede fracasar si se solicita m´as memoria de la disponible en el ordenador. Parece l´ogico pensar que en un ordenador con 64 megabytes, de los que el sistema operativo y los programas en ejecuci´on han consumido, digamos, 16 megabytes, podamos solicitar un bloque de hasta 48 megabytes. Pero eso no est´a garantizado. Imagina que los 16 megabytes ya ocupados no est´an dispuestos contiguamente en la memoria sino que, por ejemplo, se alternan con fragmentos de memoria libre de modo que, de cada cuatro megabytes, uno est´a ocupado y tres est´an libres, como muestra esta figura: En tal caso, el bloque de memoria m´as grande que podemos obtener con malloc es de ¡s´olo tres megabytes! Decimos que la memoria est´a fragmentada para referirnos a la alternancia de bloques libres y ocupados que limita su disponibilidad. La fragmentaci´on no s´olo limita el m´aximo tama˜no de bloque que puedes solicitar, adem´as, afecta a la eficiencia con la que se ejecutan las llamadas a malloc y free. Tambi´en puedes usar NULL para inicializar punteros y dejar expl´ıcitamente claro que no se les ha reservado memoria. vector dinamico 1.c vector dinamico.c 1 #include <stdlib.h> 2 #include <stdio.h> 3 4 int main(void) 5 { 6 int * a = NULL; 7 int talla, i; 8 9 printf ("N´umero de elementos: "); scanf ("%d", &talla); 10 a = malloc( talla * sizeof(int) ); 11 for (i=0; i<talla; i++) 12 a[i] = i; 13 free(a); 14 a = NULL; 15 216 Introducci´on a la Programaci´on con C
  • 223. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica 16 return 0; 17 } Aritm´etica de punteros y recorrido de vectores La aritm´etica de punteros da lugar a expresiones idiom´aticas de C que deber´ıas saber leer. F´ıjate en este programa: vector dinamico 2.c vector dinamico.c 1 #include <stdlib.h> 2 #include <stdio.h> 3 4 int main(void) 5 { 6 int * a = NULL; 7 int talla, i; 8 int * p; 9 10 printf ("N´umero de elementos: "); scanf ("%d", &talla); 11 a = malloc( talla * sizeof(int) ); 12 for (i=0, p=a; i<talla; i++, p++) 13 *p = i; 14 free(a); 15 a = NULL; 16 17 return 0; 18 } El efecto del bucle es inicializar el vector con la secuencia 0, 1, 2. . . El puntero p empieza apuntando a donde a, o sea, al principio del vector. Con cada autoincremento, p++, pasa a apuntar a la siguiente celda. Y la sentencia *p = i asigna al lugar apuntado por p el valor i. 4.1.2. Algunos ejemplos Es hora de poner en pr´actica lo aprendido desarrollando un par de ejemplos. Creaci´on de un nuevo vector con una selecci´on, de talla desconocida, de elementos de otro vector Empezaremos por dise˜nar una funci´on que recibe un vector de enteros, selecciona aquellos cuyo valor es par y los devuelve en un nuevo vector cuya memoria se solicita din´amicamente. 1 int * selecciona_pares(int a[], int talla) 2 { 3 int i, j, numpares = 0; 4 int * pares; 5 6 // Primero hemos de averiguar cu´antos elementos pares hay en a. 7 for (i=0; i<talla; i++) 8 if (a[i] % 2 == 0) 9 numpares++; 10 11 // Ahora podemos pedir memoria para ellos. 12 pares = malloc( numpares * sizeof(int) ); 13 14 // Y, finalmente, copiar los elementos pares en la zona de memoria solicitada. 15 j = 0; 16 for (i=0; i<talla; i++) 17 if (a[i] % 2 == 0) 18 pares[j++] = a[i]; 19 Introducci´on a la Programaci´on con C 217
  • 224. 4.1 Vectores din´amicos 20 return pares; 21 } Observa que devolvemos un dato de tipo int *, es decir, un puntero a entero; bueno, en realidad se trata de un puntero a una secuencia de enteros (recuerda que son conceptos equivalentes en C). Es la forma que tenemos de devolver vectores desde una funci´on. Este programa, por ejemplo, llama a selecciona_pares: pares.c 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <time.h> 4 5 #define TALLA 10 6 . . . 27 } 28 29 int main(void) 30 { 31 int vector[TALLA], i; 32 int * seleccion; 33 34 // Llenamos el vector con valores aleatorios. 35 srand(time(0)); 36 for (i=0; i<TALLA; i++) 37 vector[i] = rand(); 38 39 // Se efect´ua ahora la selecci´on de pares. 40 seleccion = selecciona_pares(vector, TALLA); 41 // La variable seleccion apunta ahora a la zona de memoria con los elementos pares. 42 43 // S´ı, pero, ? cu´antos elementos pares hay? 44 for (i=0; i<????; i++) 45 printf ("%dn", seleccion[i]); 46 47 free(seleccion); 48 seleccion = NULL; 49 50 return 0; 51 } Tenemos un problema al usar selecciona_pares: no sabemos cu´antos valores ha seleccionado. Podemos modificar la funci´on para que modifique el valor de un par´ametro que pasamos por referencia: 1 int * selecciona_pares(int a[], int talla, int * numpares ) 2 { 3 int i, j; 4 int * pares; 5 6 // Contamos el n´umero de elementos pares en el par´ametro numpares, pasado por referencia. 7 *numpares = 0; 8 for (i=0; i<talla; i++) 9 if (a[i] % 2 == 0) 10 (*numpares)++; 11 12 pares = malloc( *numpares * sizeof(int) ); 13 14 j = 0; 15 for (i=0; i<talla; i++) 16 if (a[i] % 2 == 0) 17 pares[j++] = a[i]; 218 Introducci´on a la Programaci´on con C
  • 225. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica 18 19 return pares; 20 } Ahora podemos resolver el problema: pares.c pares.c 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <time.h> 4 5 #define TALLA 10 6 7 int * selecciona_pares(int a[], int talla, int * numpares) 8 { 9 int i, j; 10 int * pares; 11 12 // Contamos el n´umero de elementos pares en el par´ametro numpares, pasado por referencia. 13 *numpares = 0; 14 for (i=0; i<talla; i++) 15 if (a[i] % 2 == 0) 16 (*numpares)++; 17 18 pares = malloc( *numpares * sizeof(int) ); 19 20 j = 0; 21 for (i=0; i<talla; i++) 22 if (a[i] % 2 == 0) 23 pares[j++] = a[i]; 24 25 return pares; 26 } 27 28 int main(void) 29 { 30 int vector[TALLA], i; 31 int * seleccion, seleccionados ; 32 33 // Llenamos el vector con valores aleatorios. 34 srand(time(0)); 35 for (i=0; i<TALLA; i++) 36 vector[i] = rand(); 37 38 // Se efect´ua ahora la selecci´on de pares. 39 seleccion = selecciona_pares(vector, TALLA, &seleccionados ); 40 // La variable seleccion apunta ahora a la zona de memoria con los elementos pares. 41 // Adem´as, la variable seleccionados contiene el n´umero de pares. 42 43 // Ahora los mostramos en pantalla. 44 for (i=0; i<seleccionados ; i++) 45 printf ("%dn", seleccion[i]); 46 47 free(seleccion); 48 seleccion = NULL; 49 50 return 0; 51 } Por cierto, el prototipo de la funci´on, que es ´este: int * selecciona_pares(int a[], int talla, int * seleccionados); puede cambiarse por este otro: int * selecciona_pares(int * a, int talla, int * seleccionados); Introducci´on a la Programaci´on con C 219
  • 226. 4.1 Vectores din´amicos Conceptualmente, es lo mismo un par´ametro declarado como int a[] que como int * a: ambos son, en realidad, punteros a enteros3 . No obstante, es preferible utilizar la primera forma cuando un par´ametro es un vector de enteros, ya que as´ı lo distinguimos f´acilmente de un entero pasado por referencia. Si ves el ´ultimo prototipo, no hay nada que te permita saber si a es un vector o un entero pasado por referencia como seleccionados. Es m´as legible, pues, la primera forma. No puedes devolver punteros a datos locales Como un vector de enteros y un puntero a una secuencia de enteros son, en cierto modo, equivalentes, puede que esta funci´on te parezca correcta: int * primeros(void) { int i, v[10]; for (i=0; i<10; i++) v[i] = i + 1; return v; } La funci´on devuelve, a fin de cuentas, una direcci´on de memoria en la que empieza una secuencia de enteros. Y es verdad: eso es lo que hace. El problema radica en que la memoria a la que apunta ¡no ((existe)) fuera de la funci´on! La memoria que ocupa v se libera tan pronto finaliza la ejecuci´on de la funci´on. Este intento de uso de la funci´on, por ejemplo, trata de acceder ilegalmente a memoria: int main(void) { int * a; a = primeros(); printf ("%d ", a[i]); // No existe a[i]. } Recuerda: si devuelves un puntero, ´este no puede apuntar a datos locales. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 219 Dise˜na una funci´on que seleccione todos los n´umeros positivos de un vector de enteros. La funci´on recibir´a el vector original y un par´ametro con su longitud y devolver´a dos datos: un puntero al nuevo vector de enteros positivos y su longitud. El puntero se devolver´a como valor de retorno de la funci´on, y la longitud mediante un par´ametro adicional (un entero pasado por referencia). · 220 Desarrolla una funci´on que seleccione todos los n´umeros de un vector de float mayores que un valor dado. Dise˜na un programa que llame correctamente a la funci´on y muestre por pantalla el resultado. · 221 Escribe un programa que lea por teclado un vector de float cuyo tama˜no se solicitar´a previamente al usuario. Una vez le´ıdos los componentes del vector, el programa copiar´a sus valores en otro vector distinto que ordenar´a con el m´etodo de la burbuja. Recuerda liberar toda memoria din´amica solicitada antes de finalizar el programa. · 222 Escribe una funci´on que lea por teclado un vector de float cuyo tama˜no se solicitar´a previamente al usuario. Escribe, adem´as, una funci´on que reciba un vector como el le´ıdo en la funci´on anterior y devuelva una copia suya con los mismos valores, pero ordenados de menor a mayor (usa el m´etodo de ordenaci´on de la burbuja o cualquier otro que conozcas). Dise˜na un programa que haga uso de ambas funciones. Recuerda que debes liberar toda memoria din´amica solicitada antes de finalizar la ejecuci´on del programa. · 223 Escribe una funci´on que reciba un vector de enteros y devuelva otro con sus n mayores valores, siendo n un n´umero menor o igual que la talla del vector original. 3En realidad, hay una peque˜na diferencia. La declaraci´on int a[] hace que a sea un puntero inmutable, mientras que int * a permite modificar la direcci´on apuntada por a haciendo, por ejemplo, a++. De todos modos, no haremos uso de esa diferencia en este texto. 220 Introducci´on a la Programaci´on con C
  • 227. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica · 224 Escribe una funci´on que reciba un vector de enteros y un valor n. Si n es menor o igual que la talla del vector, la funci´on devolver´a un vector con las n primeras celdas del vector original. En caso contrario, devolver´a un vector de n elementos con un copia del contenido del original y con valores nulos hasta completarlo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . No resulta muy elegante que una funci´on devuelva valores mediante return y, a la vez, me- diante par´ametros pasados por referencia. Una posibilidad es usar ´unicamente valores pasados por referencia: pares 1.c pares.c 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <time.h> 4 5 #define TALLA 10 6 7 void selecciona_pares(int a[], int talla, int * pares[], int * numpares) 8 { 9 int i, j; 10 11 *numpares = 0; 12 for (i=0; i<talla; i++) 13 if (a[i] % 2 == 0) 14 (*numpares)++; 15 16 *pares = malloc(*numpares * sizeof(int) ); 17 18 j = 0; 19 for (i=0; i<talla; i++) 20 if (a[i] % 2 == 0) 21 (*pares)[j++] = a[i]; 22 } 23 24 int main(void) 25 { 26 int vector[TALLA], i; 27 int * seleccion , seleccionados; 28 29 srand(time(0)); 30 for (i=0; i<TALLA; i++) 31 vector[i] = rand(); 32 33 selecciona_pares(vector, TALLA, &seleccion , &seleccionados); 34 35 for (i=0; i<seleccionados; i++) 36 printf ("%dn", seleccion[i]); 37 38 free(seleccion); 39 seleccion = NULL; 40 41 return 0; 42 } F´ıjate en la declaraci´on del par´ametro pares en la l´ınea 7: es un puntero a un vector de enteros, o sea, un vector de enteros cuya direcci´on se suministra a la funci´on. ¿Por qu´e? Porque a resultas de llamar a la funci´on, la direcci´on apuntada por pares ser´a una ((nueva)) direcci´on (la que obtengamos mediante una llamada a malloc). La l´ınea 16 asigna un valor a *pares. Resulta interesante que veas c´omo se asigna valores al vector apuntado por *pares en la l´ınea 21 (los par´entesis alrededor de *pares son obligatorios). Finalmente, observa que seleccion se declara en la l´ınea 27 como un puntero a entero y que se pasa la direcci´on en la que se almacena dicho puntero en la llamada a selecciona_pares desde la l´ınea 33. Hay una forma alternativa de indicar que pasamos la direcci´on de memoria de un puntero de enteros. La cabecera de la funci´on selecciona_pares podr´ıa haberse definido as´ı: Introducci´on a la Programaci´on con C 221
  • 228. 4.1 Vectores din´amicos void selecciona_pares(int a[], int talla, int ** pares , int * numpares) ¿Ves c´omo usamos un doble asterisco? Valores de retorno como aviso de errores Es habitual que aquellas funciones C que pueden dar lugar a errores nos adviertan de ellos mediante el valor de retorno. La funci´on malloc, por ejemplo, devuelve el valor NULL cuando no consigue reservar la memoria solicitada y un valor diferente cuando s´ı lo consigue. La funci´on scanf , que hemos estudiado como si no devolviese valor alguno, s´ı lo hace: devuelve el n´umero de elementos cuyo valor ha sido efectivamente le´ıdo. Si, por ejemplo, llamamos a scanf ("%d %d", &a, &b), la funci´on devuelve el valor 2 si todo fue bien (se ley´o el contenido de dos variables). Si devuelve el valor 1, es porque s´olo consigui´o leer el valor de a, y si devuelve el valor 0, no consigui´o leer ninguno de los dos. Un programa robusto debe comprobar el valor devuelto siempre que se efect´ue una llamada a scanf ; as´ı: 1 if ( scanf ("%d %d", &a, &b) != 2) 2 printf ("Error! No consegu´ı leer los valores de a y b.n"); 3 else { 4 // Situaci´on normal. 5 ... 6 } Las rutinas que nosotros dise˜namos deber´ıan presentar un comportamiento similar. La fun- ci´on selecciona_pares, por ejemplo, podr´ıa implementarse as´ı: 1 int selecciona_pares(int a[], int talla, int * pares[], int * numpares) 2 { 3 int i, j; 4 5 *numpares = 0; 6 for (i=0; i<talla; i++) 7 if (a[i] % 2 == 0) 8 (*numpares)++; 9 *pares = malloc(*numpares * sizeof(int) ); 10 if (*pares == NULL) { // Algo fue mal: no conseguimos la memoria. 11 *numpares = 0; // Informamos de que el vector tiene capacidad 0... 12 return 0; // y devolvemos el valor 0 para advertir de que hubo un error. 13 } 14 j = 0; 15 for (i=0; i<talla; i++) 16 if (a[i] % 2 == 0) 17 (*pares)[j++] = a[i]; 18 return 1; // Si llegamos aqu´ı, todo fue bien, as´ı que avisamos de ello con el valor 1. 19 } Aqu´ı tienes un ejemplo de uso de la nueva funci´on: 1 if ( selecciona_pares(vector, TALLA, &seleccion , &seleccionados) ) { 2 // Todo va bien. 3 } 4 else { 5 // Algo fue mal. 6 } Hay que decir, no obstante, que esta forma de aviso de errores empieza a quedar obsoleto. Los lenguajes de programaci´on m´as modernos, como C++ o Python, suelen basar la detecci´on (y el tratamiento) de errores en las denominadas ((excepciones)). M´as elegante resulta definir un registro ((vector din´amico de enteros)) que almacene conjun- tamente tanto el vector de elementos propiamente dicho como el tama˜no del vector4 : 4Aunque recomendemos este nuevo m´etodo para gestionar vectores de tama˜no variable, has de saber, cuando menos, leer e interpretar correctamente par´ametros con tipos como int a[], int *a, int *a[] o int **a, pues muchas veces tendr´as que utilizar bibliotecas escritas por otros programadores o leer c´odigo fuente de programas cuyos dise˜nadores optaron por estos estilos de paso de par´ametros. 222 Introducci´on a la Programaci´on con C
  • 229. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica pares 2.c pares.c 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <time.h> 4 5 struct VectorDinamicoEnteros { 6 int * elementos; // Puntero a la zona de memoria con los elementos. 7 int talla; // N´umero de enteros almacenados en esa zona de memoria. 8 }; 9 10 struct VectorDinamicoEnteros selecciona_pares(struct VectorDinamicoEnteros entrada) 11 // Recibe un vector din´amico y devuelve otro con una selecci´on de los elementos 12 // pares del primero. 13 { 14 int i, j; 15 struct VectorDinamicoEnteros pares; 16 17 pares.talla = 0; 18 for (i=0; i<entrada.talla; i++) 19 if (entrada.elementos[i] % 2 == 0) 20 pares.talla++; 21 22 pares.elementos = malloc(pares.talla * sizeof(int) ); 23 24 j = 0; 25 for (i=0; i<entrada.talla; i++) 26 if (entrada.elementos[i] % 2 == 0) 27 pares.elementos[j++] = entrada.elementos[i]; 28 29 return pares; 30 } 31 32 int main(void) 33 { 34 int i; 35 struct VectorDinamicoEnteros vector, seleccionados; 36 37 vector.talla = 10; 38 vector.elementos = malloc(vector.talla * sizeof(int)); 39 srand(time(0)); 40 for (i=0; i<vector.talla; i++) 41 vector.elementos[i] = rand(); 42 43 seleccionados = selecciona_pares(vector); 44 45 for (i=0; i<seleccionados.talla; i++) 46 printf ("%dn", seleccionados.elementos[i]); 47 48 free(seleccionados.elementos); 49 seleccionados.elementos = NULL; 50 seleccionados.talla = 0; 51 52 return 0; 53 } El ´unico problema de esta aproximaci´on es la potencial fuente de ineficiencia que supone devolver una copia de un registro, pues podr´ıa ser de gran tama˜no. No es nuestro caso: un struct VectorDinamicoEnteros ocupa s´olo 8 bytes. Si el tama˜no fuera un problema, podr´ıamos usar una variable de ese tipo como par´ametro pasado por referencia. Usar´ıamos as´ı s´olo 4 bytes: pares 3.c pares.c 1 #include <stdio.h> 2 #include <stdlib.h> Introducci´on a la Programaci´on con C 223
  • 230. 4.1 Vectores din´amicos 3 4 struct VectorDinamicoEnteros { 5 int * elementos; 6 int talla; 7 }; 8 9 void selecciona_pares(struct VectorDinamicoEnteros entrada, 10 struct VectorDinamicoEnteros * pares) 11 { 12 int i, j; 13 14 pares->talla = 0; 15 for (i=0; i<entrada.talla; i++) 16 if (entrada.elementos[i] % 2 == 0) 17 pares->talla++; 18 19 pares->elementos = malloc(pares->talla * sizeof(int) ); 20 21 j = 0; 22 for (i=0; i<entrada.talla; i++) 23 if (entrada.elementos[i] % 2 == 0) 24 pares->elementos[j++] = entrada.elementos[i]; 25 } 26 27 int main(void) 28 { 29 int i; 30 struct VectorDinamicoEnteros vector, seleccionados; 31 32 vector.talla = 10; 33 vector.elementos = malloc(vector.talla * sizeof(int)); 34 for (i=0; i<vector.talla; i++) 35 vector.elementos[i] = rand(); 36 37 selecciona_pares(vector, &seleccionados); 38 39 for (i=0; i<seleccionados.talla; i++) 40 printf ("%dn", seleccionados.elementos[i]); 41 42 free(seleccionados.elementos); 43 seleccionados.elementos = NULL; 44 seleccionados.talla = 0; 45 46 return 0; 47 } Como ves, tienes muchas soluciones t´ecnicamente diferentes para realizar lo mismo. Deber´as elegir en funci´on de la elegancia de cada soluci´on y de su eficiencia. Listas Python Empieza a quedar claro que Python es un lenguaje mucho m´as c´omodo que C para gestionar vectores din´amicos, que all´ı denomin´abamos listas. No obstante, debes tener presente que el int´erprete de Python est´a escrito en C, as´ı que cuando manejas listas Python est´as, indirectamente, usando memoria din´amica como malloc y free. Cuando creas una lista Python con una orden como a = [0] * 5 o a = [0, 0, 0, 0, 0], est´as reservando espacio en memoria para 5 elementos y asign´andole a cada elemento el valor 0. La variable a puede verse como un simple puntero a esa zona de memoria (en realidad es algo m´as complejo). Cuando se pierde la referencia a una lista (por ejemplo, cambiando el valor asignado a a), Python se encarga de detectar autom´aticamente que la lista ya no es apuntada por nadie y de llamar a free para que la memoria que hasta ahora ocupaba pase a quedar libre. 224 Introducci´on a la Programaci´on con C
  • 231. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica Representaci´on de pol´ıgonos con un n´umero arbitrario de v´ertices Desarrollemos un ejemplo m´as: un programa que lea los v´ertices de un pol´ıgono y calcule su per´ımetro. Empezaremos por crear un tipo de datos para almacenar los puntos de un pol´ıgono. Nuestro tipo de datos se define as´ı: struct Punto { float x, y; }; struct Poligono { struct Punto * p; int puntos; }; F´ıjate en que un pol´ıgono presenta un n´umero de puntos inicialmente desconocido, por lo que hemos de recurrir a memoria din´amica. Reservaremos la memoria justa para guardar dichos puntos en el campo p (un puntero a una secuencia de puntos) y el n´umero de puntos se almacenar´a en el campo puntos. Aqu´ı tienes una funci´on que lee un pol´ıgono por teclado y devuelve un registro con el resultado: 1 struct Poligono lee_poligono(void) 2 { 3 int i; 4 struct Poligono pol; 5 6 printf ("N´umero de puntos: "); scanf ("%d", &pol.puntos); 7 pol.p = malloc( pol.puntos * sizeof(struct Punto)); 8 for (i=0; i<pol.puntos; i++) { 9 printf ("Punto %dn", i); 10 printf ("x: "); scanf ("%f", &pol.p[i].x); 11 printf ("y: "); scanf ("%f", &pol.p[i].y); 12 } 13 return pol; 14 } Es interesante la forma en que solicitamos memoria para el vector de puntos: pol.p = malloc( pol.puntos * sizeof(struct Punto)); Solicitamos memoria para pol.puntos celdas, cada una con capacidad para un dato de tipo struct Punto (es decir, ocupando sizeof(struct Punto) bytes). Nos vendr´a bien una funci´on que libere la memoria solicitada para almacenar un pol´ıgono, ya que, de paso, pondremos el valor correcto en el campo puntos: 1 void libera_poligono(struct Poligono * pol) 2 { 3 free (pol->p); 4 pol->p = NULL; 5 pol->puntos = 0; 6 } Vamos ahora a definir una funci´on que calcula el per´ımetro de un pol´ıgono: 1 float perimetro_poligono(struct Poligono pol) 2 { 3 int i; 4 float perim = 0.0; 5 6 for (i=1; i<pol.puntos; i++) 7 perim += sqrt( (pol.p[i].x - pol.p[i-1].x) * (pol.p[i].x - pol.p[i-1].x) + 8 (pol.p[i].y - pol.p[i-1].y) * (pol.p[i].y - pol.p[i-1].y) ); 9 perim += sqrt( (pol.p[pol.puntos-1].x - pol.p[0].x) * (pol.p[pol.puntos-1].x - pol.p[0].x) + 10 (pol.p[pol.puntos-1].y - pol.p[0].y) * (pol.p[pol.puntos-1].y - pol.p[0].y) ); 11 return perim; 12 } Introducci´on a la Programaci´on con C 225
  • 232. 4.1 Vectores din´amicos Es importante que entiendas bien expresiones como pol.p[i].x. Esa, en particular, significa: del par´ametro pol, que es un dato de tipo struct Poligono, accede al componente i del campo p, que es un vector de puntos; dicho componente es un dato de tipo struct Punto, pero s´olo nos interesa acceder a su campo x (que, por cierto, es de tipo float). Juntemos todas las piezas y a˜nadamos un sencillo programa principal que invoque a las funciones desarrolladas: polinomios dinamicos.c polinomios dinamicos.c 1 #include <stdio.h> 2 #include <stdlib.h> 3 4 struct Punto { 5 float x, y; 6 }; 7 8 struct Poligono { 9 struct Punto * p; 10 int puntos; 11 }; 12 13 struct Poligono lee_poligono(void) 14 { 15 int i; 16 struct Poligono pol; 17 18 printf ("N´umero de puntos: "); scanf ("%d", &pol.puntos); 19 pol.p = malloc( pol.puntos * sizeof(struct Punto)); 20 for (i=0; i<pol.puntos; i++) { 21 printf ("Punto %dn", i); 22 printf ("x: "); scanf ("%f", &pol.p[i].x); 23 printf ("y: "); scanf ("%f", &pol.p[i].y); 24 } 25 return pol; 26 } 27 28 void libera_poligono(struct Poligono * pol) 29 { 30 free (pol->p); 31 pol->p = NULL; 32 pol->puntos = 0; 33 } 34 35 float perimetro_poligono(struct Poligono pol) 36 { 37 int i; 38 float perim = 0.0; 39 40 for (i=1; i<pol.puntos; i++) 41 perim += sqrt( (pol.p[i].x - pol.p[i-1].x) * (pol.p[i].x - pol.p[i-1].x) + 42 (pol.p[i].y - pol.p[i-1].y) * (pol.p[i].y - pol.p[i-1].y) ); 43 perim += sqrt( (pol.p[pol.puntos-1].x - pol.p[0].x) * (pol.p[pol.puntos-1].x - pol.p[0].x) + 44 (pol.p[pol.puntos-1].y - pol.p[0].y) * (pol.p[pol.puntos-1].y - pol.p[0].y) ); 45 return perim; 46 } 47 48 int main(void) 49 { 50 struct Poligono un_poligono; 51 float perimetro; 52 53 un_poligono = lee_poligono(); 54 perimetro = perimetro_poligono(un_poligono); 55 printf ("Per´ımetro %fn", perimetro); 226 Introducci´on a la Programaci´on con C
  • 233. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica 56 libera_poligono(&un_poligono); 57 58 return 0; 59 } No es el ´unico modo en que podr´ıamos haber escrito el programa. Te presentamos ahora una implementaci´on con bastantes diferencias en el modo de paso de par´ametros: polinomios dinamicos 1.c polinomios dinamicos.c 1 #include <stdio.h> 2 #include <stdlib.h> 3 4 struct Punto { 5 float x, y; 6 }; 7 8 struct Poligono { 9 struct Punto * p; 10 int puntos; 11 }; 12 13 void lee_poligono(struct Poligono * pol) 14 { 15 int i; 16 17 printf ("N´umero de puntos: "); scanf ("%d", &pol->puntos); 18 pol->p = malloc( pol->puntos * sizeof(struct Punto)); 19 for (i=0; i<pol->puntos; i++) { 20 printf ("Punto %dn", i); 21 printf ("x: "); scanf ("%f", &pol->p[i].x); 22 printf ("y: "); scanf ("%f", &pol->p[i].y); 23 } 24 } 25 26 void libera_poligono(struct Poligono * pol) 27 { 28 free (pol->p); 29 pol->p = NULL; 30 pol->puntos = 0; 31 } 32 33 float perimetro_poligono(const struct Poligono * pol) 34 { 35 int i; 36 float perim = 0.0; 37 38 for (i=1; i<pol->puntos; i++) 39 perim += sqrt( (pol->p[i].x - pol->p[i-1].x) * (pol->p[i].x - pol->p[i-1].x) + 40 (pol->p[i].y - pol->p[i-1].y) * (pol->p[i].y - pol->p[i-1].y) ); 41 perim += 42 sqrt((pol->p[pol->puntos-1].x - pol->p[0].x) * (pol->p[pol->puntos-1].x - pol->p[0].x) + 43 (pol->p[pol->puntos-1].y - pol->p[0].y) * (pol->p[pol->puntos-1].y - pol->p[0].y) ); 44 return perim; 45 } 46 47 int main(void) 48 { 49 struct Poligono un_poligono; 50 float perimetro; 51 52 lee_poligono(&un_poligono); 53 perimetro = perimetro_poligono(&un_poligono); 54 printf ("Per´ımetro %fn", perimetro); 55 libera_poligono(&un_poligono); Introducci´on a la Programaci´on con C 227
  • 234. 4.1 Vectores din´amicos 56 57 return 0; 58 } En esta versi´on hemos optado, siempre que ha sido posible, por el paso de par´ametros por referencia, es decir, por pasar la direcci´on de la variable en lugar de una copia de su contenido. Hay una raz´on para hacerlo: la eficiencia. Cada dato de tipo struct Poligono esta formado por un puntero (4 bytes) y un entero (4 bytes), as´ı que ocupa 8 bytes. Si pasamos o devolvemos una copia de un struct Poligono, estamos copiando 8 bytes. Si, por contra, pasamos su direcci´on de memoria, s´olo hay que pasar 4 bytes. En este caso particular no hay una ganancia extraordinaria, pero en otras aplicaciones manejar´as structs tan grandes que el paso de la direcci´on compensar´a la ligera molestia de la notaci´on de acceso a campos con el operador ->. Puede que te extra˜ne el t´ermino const calificando el par´ametro de perimetro_poligono. Su uso es opcional y sirve para indicar que, aunque es posible modificar la informaci´on apuntada por pol, no lo haremos. En realidad suministramos el puntero por cuesti´on de eficiencia, no porque deseemos modificar el contenido. Con esta indicaci´on conseguimos dos efectos: si intent´asemos modificar accidentalmente el contenido, el compilador nos advertir´ıa del error; y, si fuera posible, el compilador efectuar´ıa optimizaciones que no podr´ıa aplicar si la informaci´on apuntada por pol pudiera modificarse en la funci´on. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 225 ¿Funciona esta otra implementaci´on de perimetro_poligono? 1 float perimetro_poligono(struct Poligono pol) 2 { 3 int i; 4 float perim = 0.0; 5 6 for (i=1; i<pol.puntos +1; i++) 7 perim += 8 sqrt((pol.p[i%pol.puntos ].x - pol.p[i-1].x) * (pol.p[i%pol.puntos ].x - pol.p[i-1].x)+ 9 (pol.p[i%pol.puntos ].y - pol.p[i-1].y) * (pol.p[i%pol.puntos ].y - pol.p[i-1].y)); 10 return perim; 11 } · 226 Dise˜na una funci´on que cree un pol´ıgono regular de n lados inscrito en una circunfe- rencia de radio r. Esta figura muestra un pent´agono inscrito en una circunferencia de radio r y las coordenadas de cada uno de sus v´ertices: (r cos(0), r sin(0)) (r cos(2π/5), r sin(2π/5)) (r cos(2π · 2/5), r sin(2π · 2/5)) (r cos(2π · 3/5), r sin(2π · 3/5)) (r cos(2π · 4/5), r sin(2π · 4/5)) r Utiliza la funci´on para crear pol´ıgonos regulares de talla 3, 4, 5, 6, . . . inscritos en una circunferencia de radio 1. Calcula a continuaci´on el per´ımetro de los sucesivos pol´ıgonos y comprueba si dicho valor se aproxima a 2π. · 227 Dise˜na un programa que permita manipular polinomios de cualquier grado. Un poli- nomio se representar´a con el siguiente tipo de registro: 1 struct Polinomio { 2 float * p; 3 int grado; 4 }; Como puedes ver, el campo p es un puntero a float, o sea, un vector din´amico de float. Dise˜na y utiliza funciones que hagan lo siguiente: 228 Introducci´on a la Programaci´on con C
  • 235. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica Leer un polinomio por teclado. Se pedir´a el grado del polinomio y, tras reservar memoria suficiente para sus coeficientes, se pedir´a tambi´en el valor de cada uno de ellos. Evaluar un polinomio p(x) para un valor dado de x. Sumar dos polinomios. Ten en cuenta que cada uno de ellos puede ser de diferente grado y el resultado tendr´a, en principio, grado igual que el mayor grado de los operandos. (Hay excepciones; piensa cu´ales.) Multiplicar dos polinomios. · 228 Dise˜na un programa que solicite la talla de una serie de valores enteros y dichos valores. El programa ordenar´a a continuaci´on los valores mediante el procedimiento mergesort. (Ten en cuenta que el vector auxiliar que necesita merge debe tener capacidad para el mismo n´umero de elementos que el vector original.) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Reserva con inicializaci´on autom´atica La funci´on calloc es similar a malloc, pero presenta un prototipo diferente y hace algo m´as que reservar memoria: la inicializa a cero. He aqu´ı un prototipo (similar al) de calloc: void * calloc(int nmemb, int size); Con calloc, puedes pedir memoria para un vector de talla enteros as´ı: a = calloc(talla, sizeof(int)); El primer par´ametro es el n´umero de elementos y el segundo, el n´umero de bytes que ocupa cada elemento. No hay que multiplicar una cantidad por otra, como hac´ıamos con malloc. Todos los enteros del vector se inicializan a cero. Es como si ejecut´asemos este fragmento de c´odigo: a = malloc( talla * sizeof(int) ); for (i = 0; i < talla; i++) a[i] = 0; ¿Por qu´e no usar siempre calloc, si parece mejor que malloc? Por eficiencia. En ocasiones no desear´as que se pierda tiempo de ejecuci´on inicializando la memoria a cero, ya que t´u mismo querr´as inicializarla a otros valores inmediatamente. Recuerda que garantizar la mayor eficiencia de los programas es uno de los objetivos del lenguaje de programaci´on C. 4.1.3. Cadenas din´amicas Las cadenas son un caso particular de vector. Podemos usar cadenas de cualquier longitud gracias a la gesti´on de memoria din´amica. Este programa, por ejemplo, lee dos cadenas y construye una nueva que resulta de concatenar a ´estas. cadenas dinamicas.c cadenas dinamicas.c 1 #include <stdio.h> 2 #include <string.h> 3 #include <stdlib.h> 4 5 #define CAPACIDAD 80 6 7 int main(void) 8 { 9 char cadena1[CAPACIDAD+1], cadena2[CAPACIDAD+1]; 10 char * cadena3; 11 12 printf ("Dame un texto: "); gets(cadena1); 13 printf ("Dame otro texto: "); gets(cadena2); 14 15 cadena3 = malloc( (strlen(cadena1) + strlen(cadena2) + 1) * sizeof(char) ); Introducci´on a la Programaci´on con C 229
  • 236. 4.2 Matrices din´amicas 16 17 strcpy(cadena3, cadena1); 18 strcat(cadena3, cadena2); 19 20 printf ("Resultado de concatenar ambos: %sn", cadena3); 21 22 free(cadena3); 23 cadena3 = NULL; 24 25 return 0; 26 } Como las dos primeras cadenas se leen con gets, hemos de definirlas como cadenas est´aticas. La tercera cadena reserva exactamente la misma cantidad de memoria que ocupa. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 229 Dise˜na una funci´on que lea una cadena y construya otra con una copia invertida de la primera. La segunda cadena reservar´a s´olo la memoria que necesite. · 230 Dise˜na una funci´on que lea una cadena y construya otra que contenga un ejemplar de cada car´acter de la primera. Por ejemplo, si la primera cadena es "este ejemplo", la segunda ser´a "est jmplo". Ten en cuenta que la segunda cadena debe ocupar la menor cantidad de memoria posible. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sobre la mutabilidad de las cadenas Es posible inicializar un puntero a cadena de modo que apunte a un literal de cadena: char * p = "cadena"; Pero, ¡ojo!, la cadena apuntada por p es, en ese caso, inmutable: si intentas asignar un char a p[i], el programa puede abortar su ejecuci´on. ¿Por qu´e? Porque los literales de cadena ((residen)) en una zona de memoria especial (la denominada ((zona de texto))) que est´a protegida contra escritura. Y hay una raz´on para ello: en esa zona reside, tambi´en, el c´odigo de m´aquina correspondiente al programa. Que un programa modifique su propio c´odigo de m´aquina es una p´esima pr´actica (que era relativamente frecuente en los tiempos en que predominaba la programaci´on en ensamblador), hasta el punto de que su zona de memoria se marca como de s´olo lectura. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 231 Implementa una funci´on que reciba una cadena y devuelva una copia invertida. (Ten en cuenta que la talla de la cadena puede conocerse con strlen, as´ı que no es necesario que suministres la talla expl´ıcitamente ni que devuelvas la talla de la memoria solicitada con un par´ametro pasado por referencia.) Escribe un programa que solicite varias palabras a un usuario y muestre el resultado de invertir cada una de ellas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2. Matrices din´amicas Podemos extender la idea de los vectores din´amicos a matrices din´amicas. Pero el asunto se com- plica notablemente: no podemos gestionar la matriz como una sucesi´on de elementos contiguos, sino como un ((vector din´amico de vectores din´amicos)). 4.2.1. Gesti´on de memoria para matrices din´amicas Analiza detenidamente este programa: 230 Introducci´on a la Programaci´on con C
  • 237. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica matriz dinamica.c matriz dinamica.c 1 #define <stdio.h> 2 #define <stdlib.h> 3 4 int main(void) 5 { 6 float ** m = NULL; 7 int filas, columnas; 8 9 printf ("Filas: "); scanf ("%d", &filas); 10 printf ("Columnas: "); scanf ("%d", &columnas); 11 12 /* reserva de memoria */ 13 m = malloc(filas * sizeof(float *)); 14 for (i=0; i<filas; i++) 15 m[i] = malloc(columnas * sizeof(float)); 16 17 /* trabajo con m[i][j] */ 18 ... 19 20 /* liberaci´on de memoria */ 21 for (i=0; i<filas; i++) 22 free(m[i]); 23 free(m); 24 m = NULL; 25 26 return 0; 27 } Analicemos poco a poco el programa. Declaraci´on del tipo Empecemos por la declaraci´on de la matriz (l´ınea 6). Es un puntero un poco extra˜no: se declara como float ** m. Dos asteriscos, no uno. Eso es porque se trata de un puntero a un puntero de enteros o, equivalentemente, un vector din´amico de vectores din´amicos de enteros. Reserva de memoria Sigamos. Las l´ıneas 9 y 10 solicitan al usuario los valores de filas y columnas. En la l´ınea 13 encontramos una petici´on de memoria. Se solicita espacio para un n´umero filas de punteros a float. Supongamos que filas vale 4. Tras esa petici´on, tenemos la siguiente asignaci´on de memoria para m: m 0 1 2 3 El vector m es un vector din´amico cuyos elementos son punteros (del tipo float *). De momento, esos punteros no apuntan a ninguna zona de memoria reservada. De ello se encarga la l´ınea 15. Dicha l´ınea est´a en un bucle, as´ı que se ejecuta para m[0], m[1], m[2], . . . El efecto es proporcionar un bloque de memoria para cada celda de m. He aqu´ı el efecto final: Introducci´on a la Programaci´on con C 231
  • 238. 4.2 Matrices din´amicas m 0 0 1 2 3 4 1 0 1 2 3 4 2 0 1 2 3 4 3 0 1 2 3 4 Acceso a filas y elementos Bien. ¿Y c´omo se usa m ahora? ¡Como cualquier matriz! Pensemos en qu´e ocurre cuando accedemos a m[1][2]. Analicemos m[1][2] de izquierda a derecha. Primero tenemos a m, que es un puntero (tipo float **), o sea, un vector din´amico a elementos del tipo float *. El elemento m[1] es el segundo componente de m. ¿Y de qu´e tipo es? De tipo float *, un nuevo puntero o vector din´amico, pero a valores de tipo float. Si es un vector din´amico, lo podemos indexar, as´ı que es v´alido escribir m[1][2]. ¿Y de qu´e tipo es eso? De tipo float. F´ıjate: m es de tipo float **; m[1] es de tipo float *; m[1][2] es de tipo float. Con cada indexaci´on, ((desaparece)) un asterisco del tipo de datos. Liberaci´on de memoria: un free para cada malloc Sigamos con el programa. Nos resta la liberaci´on de memoria. Observa que hay una llamada a free por cada llamada a malloc realizada con anterioridad (l´ıneas 20–24). Hemos de liberar cada uno de los bloques reservados y hemos de empezar a hacerlo por los de ((segundo nivel)), es decir, por los de la forma m[i]. Si empez´asemos liberando m, cometer´ıamos un grave error: si libera- mos m antes que todos los m[i], perderemos el puntero que los referencia y, en consecuencia, ¡no podremos liberarlos! ... free(m); m = NULL; /* liberaci´on de memoria incorrecta: ? qu´e es m[i] ahora que m vale NULL? */ for (i=0; i<filas; i++) free(m[i]); } Matrices din´amicas y funciones El paso de matrices din´amicas a funciones tiene varias formas idiom´aticas que conviene que conozcas. Imagina una funci´on que recibe una matriz de enteros para mostrar su contenido por pantalla. En principio, la cabecera de la funci´on presentar´ıa este aspecto: void muestra_matriz(int ** m) El par´ametro indica que es de tipo ((puntero a punteros a enteros)). Una forma alternativa de decir lo mismo es ´esta: void muestra_matriz(int * m[]) Se lee m´as bien como ((vector de punteros a entero)). Pero ambas expresiones son sin´onimas de ((vector de vectores a entero)). Uno se siente tentado de utilizar esta otra cabecera: void muestra_matriz(int m[][]) // ! Mal! Pero no funciona. Es incorrecta. C entiende que queremos pasar una matriz est´atica y que hemos omitido el n´umero de columnas. Sigamos con la funci´on: 232 Introducci´on a la Programaci´on con C
  • 239. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica M´as eficiencia, menos reservas de memoria Te hemos ense˜nado una forma ((est´andar)) de pedir memoria para matrices din´amicas. No es la ´unica. Es m´as, no es la m´as utilizada en la pr´actica. ¿Por qu´e? Porque obliga a realizar tantas llamadas a malloc (y despu´es a free) como filas tiene la matriz m´as uno. Las llamadas a malloc pueden resultar ineficientes cuando su n´umero es grande. Es posible reservar la memoria de una matriz din´amica con s´olo dos llamadas a malloc. 1 #include <stdlib.h> 2 3 int main(void) 4 { 5 int ** m; 6 int filas, columnas; 7 8 filas = ...; 9 columnas = ...; 10 11 // Reserva de memoria. 12 m = malloc(filas * sizeof(int *)); 13 m[0] = malloc(filas * columnas * sizeof(int)); 14 for (i=1; i<filas; i++) m[i] = m[i-1] + columnas; 15 16 ... 17 // Liberaci´on de memoria. 18 free(m[0]); 19 free(m); 20 21 return 0; 22 } La clave est´a en la sentencia m[i] = m[i-1] + columnas: el contenido de m[i] pasa a ser la direcci´on de memoria columnas celdas m´as a la derecha de la direcci´on m[i-1]. He aqu´ı una representaci´on gr´afica de una matriz de 5 × 4: m 0 1 2 3 4 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 1 void muestra_matriz(int ** m ) 2 { 3 int i,j; 4 5 for (i=0; i<???; i++) { 6 for (j=0; j<???; j++) 7 printf ("%d ", m[i][j]); 8 printf ("n"); 9 } 10 } Observa que necesitamos suministrar el n´umero de filas y columnas expl´ıcitamente para saber qu´e rango de valores deben tomar i y j: 1 void muestra_matriz(int ** m, int filas, int columnas) 2 { 3 int i,j; 4 5 for (i=0; i<filas; i++) { Introducci´on a la Programaci´on con C 233
  • 240. 4.2 Matrices din´amicas 6 for (j=0; j<columnas; j++) 7 printf ("%d ", m[i][j]); 8 printf ("n"); 9 } 10 } Supongamos ahora que nos piden una funci´on que efect´ue la liberaci´on de la memoria de una matriz: 1 void libera_matriz(int ** m, int filas, int columnas) 2 { 3 int i; 4 5 for (i=0; i<filas; i++) 6 free(m[i]); 7 free(m); 8 } Ahora resulta innecesario el paso del n´umero de columnas, pues no se usa en la funci´on: 1 void libera_matriz(int ** m, int filas) 2 { 3 int i; 4 5 for (i=0; i<filas; i++) 6 free(m[i]); 7 free(m); 8 } Falta un detalle que har´ıa mejor a esta funci´on: la asignaci´on del valor NULL a m al final de todo. Para ello tenemos que pasar una referencia a la matriz, y no la propia matriz: 1 void libera_matriz(int *** m, int filas) 2 { 3 int i; 4 5 for (i=0; i<filas; i++) 6 free( (*m)[i]); 7 free(*m); 8 *m = NULL; 9 } ¡Qu´e horror! ¡Tres asteriscos en la declaraci´on del par´ametro m! C no es, precisamente, el colmo de la elegancia. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 232 Dise˜na una funci´on que reciba un n´umero de filas y un n´umero de columnas y devuelva una matriz din´amica de enteros con filas×columnas elementos. · 233 Dise˜na un procedimiento que reciba un puntero a una matriz din´amica (sin memo- ria asignada), un n´umero de filas y un n´umero de columnas y devuelva, mediante el primer par´ametro, una matriz din´amica de enteros con filas×columnas elementos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La gesti´on de matrices din´amicas considerando por separado sus tres variables (puntero a memoria, n´umero de filas y n´umero de columnas) resulta poco elegante y da lugar a funciones con par´ametros de dif´ıcil lectura. En el siguiente apartado aprender´as a usar matrices din´amicas que agrupan sus tres datos en un tipo registro definido por el usuario. 4.2.2. Definici´on de un tipo ((matriz din´amica)) y de funciones para su ges- ti´on Presentaremos ahora un ejemplo de aplicaci´on de lo aprendido: un programa que multiplica dos matrices de tallas arbitrarias. Empezaremos por definir un nuevo tipo de datos para nuestras matrices. El nuevo tipo ser´a un struct que contendr´a una matriz din´amica de float y el n´umero de filas y columnas. 234 Introducci´on a la Programaci´on con C
  • 241. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica 1 struct Matriz { 2 float ** m; 3 int filas, columnas; 4 }; Dise˜nemos ahora una funci´on que ((cree)) una matriz dado el n´umero de filas y el n´umero de columnas: 1 struct Matriz crea_matriz (int filas, int columnas) 2 { 3 struct Matriz mat; 4 int i; 5 6 if (filas <= 0 || columnas <=0) { 7 mat.filas = mat.columnas = 0; 8 mat.m = NULL; 9 return mat; 10 } 11 12 mat.filas = filas; 13 mat.columnas = columnas; 14 mat.m = malloc ( filas * sizeof(float *) ); 15 for (i=0; i<filas; i++) 16 mat.m[i] = malloc ( columnas * sizeof(float) ); 17 return mat; 18 } Hemos tenido la precauci´on de no pedir memoria si el n´umero de filas o columnas no son v´alidos. Para crear una matriz de, por ejemplo, 3 × 4, llamaremos a la funci´on as´ı: 1 struct Matriz matriz; 2 ... 3 matriz = crea_matriz(3, 4); Hay una implementaci´on alternativa de crea_matriz: 1 void crea_matriz (int filas, int columnas, struct Matriz * mat) 2 { 3 int i; 4 5 if (filas <= 0 || columnas <=0) { 6 mat->filas = mat->columnas = 0; 7 mat->m = NULL; 8 } 9 else { 10 mat->filas = filas; 11 mat->columnas = columnas; 12 mat->m = malloc ( filas * sizeof(float *) ); 13 for (i=0; i<filas; i++) 14 mat->m[i] = malloc ( columnas * sizeof(float) ); 15 } 16 } En este caso, la funci´on (procedimiento) se llamar´ıa as´ı: 1 struct Matriz matriz; 2 ... 3 crea_matriz(3, 4, &matriz); Tambi´en nos vendr´a bien disponer de un procedimiento para liberar la memoria de una matriz: 1 void libera_matriz (struct Matriz * mat) 2 { 3 int i; 4 Introducci´on a la Programaci´on con C 235
  • 242. 4.2 Matrices din´amicas 5 if (mat->m != NULL) { 6 for (i=0; i<mat->filas; i++) 7 free(mat->m[i]); 8 free(mat->m); 9 } 10 11 mat->m = NULL; 12 mat->filas = 0; 13 mat->columnas = 0; 14 } Para liberar la memoria de una matriz din´amica m, efectuaremos una llamada como ´esta: 1 libera_matriz(&m); Como hemos de leer dos matrices por teclado, dise˜nemos ahora una funci´on capaz de leer una matriz por teclado: 1 struct Matriz lee_matriz (void) 2 { 3 int i, j, filas, columnas; 4 struct Matriz mat; 5 6 printf ("Filas: "); scanf ("%d", &filas); 7 printf ("Columnas: "); scanf ("%d", &columnas); 8 9 mat = crea_matriz(filas, columnas); 10 11 for (i=0; i<filas; i++) 12 for (j=0; j<columnas; j++) { 13 printf ("Elemento [%d][%d]: ", i, j); scanf ("%f", &mat.m[i][j]); 14 } 15 return mat; 16 } Observa que hemos llamado a crea_matriz tan pronto hemos sabido cu´al era el n´umero de filas y columnas de la matriz. Y ahora, implementemos un procedimiento que muestre por pantalla una matriz: 1 void muestra_matriz (struct Matriz mat) 2 { 3 int i, j; 4 5 for (i=0; i<mat.filas; i++) { 6 for (j=0; j<mat.columnas; j++) 7 printf ("%f ", mat.m[i][j]); 8 printf ("n"); 9 } 10 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 234 En muestra_matriz hemos pasado la matriz mat por valor. ¿Cu´antos bytes se copiar´an en pila con cada llamada? · 235 Dise˜na una nueva versi´on de muestra_matriz en la que mat se pase por referencia. ¿Cu´antos bytes se copiar´an en pila con cada llamada? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Podemos proceder ya mismo a implementar una funci´on que multiplique dos matrices: 1 struct Matriz multiplica_matrices (struct Matriz a, struct Matriz b) 2 { 3 int i, j, k; 4 struct Matriz c; 5 236 Introducci´on a la Programaci´on con C
  • 243. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica 6 if (a.columnas != b.filas) { /* No se pueden multiplicar */ 7 c.filas = c.columnas = 0; 8 c.m = NULL; 9 return c; 10 } 11 c = crea_matriz(a.filas, b.columnas); 12 for (i=0; i<c.filas; i++) 13 for (j=0; j<c.columnas; j++) { 14 c.m[i][j] = 0.0; 15 for (k=0; k<a.columnas; k++) 16 c.m[i][j] += a.m[i][k] * b.m[k][j]; 17 } 18 return c; 19 } No todo par de matrices puede multiplicarse entre s´ı. El n´umero de columnas de la primera ha de ser igual al n´umero de filas de la segunda. Por eso devolvemos una matriz vac´ıa (de 0×0) cuando a.columnas es distinto de b.filas. Ya podemos construir el programa principal: 1 #include <stdio.h> 2 3 ...definici´on de funciones... 4 5 int main(void) 6 { 7 struct Matriz a, b, c; 8 9 a = lee_matriz(); 10 b = lee_matriz(); 11 c = multiplica_matrices(a, b); 12 if (c.m == NULL) 13 printf ("Las matrices no son multiplicablesn"); 14 else { 15 printf ("Resultado del producto:n"); 16 muestra_matriz(c); 17 } 18 libera_matriz(&a); 19 libera_matriz(&b); 20 libera_matriz(&c); 21 22 return 0; 23 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 236 Dise˜na una funci´on que sume dos matrices. · 237 Pasar estructuras por valor puede ser ineficiente, pues se debe obtener una copia en pila de la estructura completa (en el caso de las matrices, cada variable de tipo struct Matriz ocupa 12 bytes —un puntero y dos enteros—, cuando una referencia supone la copia de s´olo 4 bytes). Modifica la funci´on que multiplica dos matrices para que sus dos par´ametros se pasen por referencia. · 238 Dise˜na una funci´on que encuentre, si lo hay, un punto de silla en una matriz. Un punto de silla es un elemento de la matriz que es o bien el m´aximo de su fila y el m´ınimo de su columna a la vez, o bien el m´ınimo de su fila y el m´aximo de su columna a la vez. La funci´on devolver´a cierto o falso dependiendo de si hay alg´un punto de silla. Si lo hay, el valor del primer punto de silla encontrado se devolver´a como valor de un par´ametro pasado por referencia. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Introducci´on a la Programaci´on con C 237
  • 244. 4.3 M´as all´a de las matrices din´amicas 4.3. M´as all´a de las matrices din´amicas 4.3.1. Vectores de vectores de tallas arbitrarias Hemos aprendido a definir matrices din´amicas con un vector din´amico de vectores din´amicos. El primero contiene punteros que apuntan a cada columna. Una caracter´ıstica de las matrices es que todas las filas tienen el mismo n´umero de elementos (el n´umero de columnas). Hay estructuras similares a las matrices pero que no imponen esa restricci´on. Pensemos, por ejemplo, en una lista de palabras. Una forma de almacenarla en memoria es la que se muestra en este gr´afico: listapal 3 2 1 0 t a c o 0 m a n o 0 d a d i v o s o 0 a n u a l 0 ¿Ves? Es parecido a una matriz, pero no exactamente una matriz: cada palabra ocupa tanta memoria como necesita, pero no m´as. Este programa solicita al usuario 4 palabras y las almacena en una estructura como la dibujada: cuatro palabras.c cuatro palabras.c 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 5 #define PALS 4 6 #define MAXLON 80 7 8 int main(void) 9 { 10 char ** listapal; 11 char linea[MAXLON+1]; 12 int i; 13 14 /* Pedir memoria y leer datos */ 15 listapal = malloc(PALS * sizeof(char *)); 16 for (i=0; i<PALS; i++) { 17 printf ("Teclea una palabra: "); 18 gets(linea); 19 listapal[i] = malloc( (strlen(linea)+1) * sizeof(char) ); 20 strcpy(listapal[i], linea); 21 } 22 23 /* Mostrar el contenido de la lista */ 24 for (i=0; i<PALS; i++) 25 printf ("Palabra %i: %sn", i, listapal[i]); 26 27 /* Liberar memoria */ 28 for (i=0; i<PALS; i++) 29 free(listapal[i]); 30 free(listapal); 31 32 return 0; 33 } Este otro programa s´olo usa memoria din´amica para las palabras, pero no para el vector de palabras: cuatro palabras 1.c cuatro palabras.c 1 #include <stdio.h> 2 #include <stdlib.h> 238 Introducci´on a la Programaci´on con C
  • 245. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica 3 #include <string.h> 4 5 #define PALS 4 6 #define MAXLON 80 7 8 int main(void) 9 { 10 char * listapal[PALS]; 11 char linea[MAXLON+1]; 12 int i; 13 14 /* Pedir memoria y leer datos */ 15 for (i=0; i<PALS; i++) { 16 printf ("Teclea una palabra: "); 17 gets(linea); 18 listapal[i] = malloc( (strlen(linea)+1) * sizeof(char) ); 19 strcpy(listapal[i], linea); 20 } 21 22 /* Mostrar el contenido de la lista */ 23 for (i=0; i<PALS; i++) 24 printf ("Palabra %i: %sn", i, listapal[i]); 25 26 /* Liberar memoria */ 27 for (i=0; i<PALS; i++) 28 free(listapal[i]); 29 30 return 0; 31 } F´ıjate en c´omo hemos definido listapal: como un vector est´atico de 4 punteros a caracteres (char * listapal[PALS]). Vamos a ilustrar el uso de este tipo de estructuras de datos con la escritura de una funci´on que reciba una cadena y devuelva un vector de palabras, es decir, vamos a implementar la funcionalidad que ofrece Python con el m´etodo split. Empecemos por considerar la cabecera de la funci´on, a la que llamaremos extrae_palabras. Est´a claro que uno de los par´ametros de entrada es una cadena, o sea, un vector de caracteres: ??? extrae_palabras(char frase[], ???) No hace falta suministrar la longitud de la cadena, pues ´esta se puede calcular con la funci´on strlen. ¿C´omo representamos la informaci´on de salida? Una posibilidad es devolver un vector de cadenas: char ** extrae_palabras(char frase[], ???) O sea, devolvemos un puntero (*) a una serie de datos de tipo char *, o sea, cadenas. Pero a´un falta algo: hemos de indicar expl´ıcitamente cu´antas palabras hemos encontrado: char ** extrae_palabras(char frase[], int * numpals) Hemos recurrido a un par´ametro adicional para devolver el segundo valor. Dicho par´ametro es la direcci´on de un entero, pues vamos a modificar su valor. Ya podemos codificar el cuerpo de la funci´on. Empezaremos por contar las palabras, que ser´an series de caracteres separadas por blancos (no entraremos en mayores complicaciones acerca de qu´e es una palabra). 1 char ** extrae_palabras(char frase[], int * numpals) 2 { 3 int i, lonfrase; 4 5 lonfrase = strlen(frase); 6 *numpals = 1; 7 for (i=0; i<lonfrase-1; i++) 8 if (frase[i] == ’ ’ && frase[i+1] != ’ ’) (*numpals)++; 9 if (frase[0] == ’ ’) (*numpals)--; 10 11 ... 12 } Introducci´on a la Programaci´on con C 239
  • 246. 4.3 M´as all´a de las matrices din´amicas Acceso a argumentos de la l´ınea de comandos Los programas que dise˜namos en el curso suponen que main no tiene par´ametros. No siempre es as´ı. La funci´on main puede recibir como argumentos las opciones que se indican en la l´ınea de comandos cuando ejecutas el programa desde la l´ınea de ´ordenes Unix. El siguiente programa muestra por pantalla un saludo personalizado y debe llamarse as´ı: saluda -n nombre Aqu´ı tienes el c´odigo fuente: saluda.c 1 #include <stdio.h> 2 #include <string.h> 3 4 main (int argc, char * argv[]) 5 { 6 if (argc != 3) 7 printf ("Error: necesito que indiques el nombre con -nn"); 8 else 9 if (strcmp(argv[1], "-n") != 0) 10 printf ("Error: s´olo entiendo la opci´on -nn"); 11 else 12 printf ("Hola, %s.", argv[2]); 13 } El argumento argc indica cu´antas ((palabras)) se han usado en la l´ınea de ´ordenes. El argumento argv es un vector de char *, es decir, un vector de cadenas (una cadena es un vector de caracteres). El elemento argv[0] contiene el nombre del programa (en nuestro caso, "saluda") que es la primera ((palabra)), argv[1] el de la segunda (que esperamos que sea "-n") y argv[2] la tercera (el nombre de la persona a la que saludamos). La estructura argv, tras la invocaci´on saluda -n nombre, es: argv 2 1 0 n o m b r e 0 - n 0 s a l u d a 0 Ya podemos reservar memoria para el vector de cadenas, pero a´un no para cada una de ellas: 1 char ** extrae_palabras(char frase[], int * numpals) 2 { 3 int i, lonfrase; 4 char **palabras; 5 6 lonfrase = strlen(frase); 7 *numpals = 1; 8 for (i=0; i<lonfrase-1; i++) 9 if (frase[i] == ’ ’ && frase[i+1] != ’ ’) (*numpals)++; 10 if (frase[0] == ’ ’) (*numpals)--; 11 12 palabras = malloc(*numpals * sizeof(char *)); 13 14 ... 15 } Ahora pasamos a reservar memoria para cada una de las palabras y, tan pronto hagamos cada reserva, ((escribirla)) en su porci´on de memoria: 1 char ** extrae_palabras(char frase[], int * numpals) 240 Introducci´on a la Programaci´on con C
  • 247. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica 2 { 3 int i, j, inicio_pal, longitud_pal, palabra_actual, lonfrase; 4 char **palabras; 5 6 lonfrase = strlen(frase); 7 *numpals = 1; 8 for (i=0; i<lonfrase-1; i++) 9 if (frase[i] == ’ ’ && frase[i+1] != ’ ’) 10 (*numpals)++; 11 if (frase[0] == ’ ’) 12 (*numpals)--; 13 14 palabras = malloc(*numpals * sizeof(char *)); 15 16 palabra_actual = 0; 17 i = 0; 18 if (frase[0] == ’ ’) 19 while (frase[++i] == ’ ’ && i < lonfrase); // Saltamos blancos iniciales. 20 21 while (i<lonfrase) { 22 inicio_pal = i; 23 while (frase[++i] != ’ ’ && i < lonfrase); // Recorremos la palabra. 24 25 longitud_pal = i - inicio_pal; // Calculamos n´umero de caracteres en la palabra actual. 26 27 palabras[palabra_actual] = malloc((longitud_pal+1)*sizeof(char)); // Reservamos memoria. 28 29 // Y copiamos la palabra de frase al vector de palabras. 30 for (j=inicio_pal; j<i; j++) 31 palabras[palabra_actual][j-inicio_pal] = frase[j]; 32 palabras[palabra_actual][j-inicio_pal] = ’0’; 33 34 while (frase[++i] == ’ ’ && i < lonfrase); // Saltamos blancos entre palabras. 35 36 palabra_actual++; // Y pasamos a la siguiente. 37 } 38 39 return palabras; 40 } ¡Buf! Complicado, ¿verdad? Veamos c´omo se puede usar la funci´on desde el programa principal: palabras.c E palabras.c E 1 #include <stdio.h> 2 #include <stdlib.h> 3 . . . 43 } 44 45 int main(void) 46 { 47 char linea[MAXLON+1]; 48 int numero_palabras, i; 49 char ** las_palabras; 50 51 printf ("Escribe una frase: "); 52 gets(linea); 53 las_palabras = extrae_palabras(linea, &numero_palabras); 54 for (i=0; i<numero_palabras; i++) 55 printf ("%sn", las_palabras[i]); 56 57 return 0; 58 } Introducci´on a la Programaci´on con C 241
  • 248. 4.3 M´as all´a de las matrices din´amicas ¿Ya? A´un no. Aunque este programa compila correctamente y se ejecuta sin problemas, hemos de considerarlo incorrecto: hemos solicitado memoria con malloc y no la hemos liberado con free. palabras 1.c palabras.c 1 #include <stdio.h> 2 #include <stdlib.h> 3 . . . 43 } 44 45 int main(void) 46 { 47 char linea[MAXLON+1]; 48 int numero_palabras, i; 49 char ** las_palabras; 50 51 printf ("Escribe una frase: "); 52 gets(linea); 53 las_palabras = extrae_palabras(linea, &numero_palabras); 54 for (i=0; i<numero_palabras; i++) 55 printf ("%sn", las_palabras[i]); 56 57 for (i=0; i<numero_palabras; i++) 58 free(las_palabras[i]); 59 free(las_palabras); 60 las_palabras = NULL; 61 62 return 0; 63 } Ahora s´ı. 4.3.2. Arreglos n-dimensionales Hemos considerado la creaci´on de estructuras bidimensionales (matrices o vectores de vectores), pero nada impide definir estructuras con m´as dimensiones. Este sencillo programa pretende ilustrar la idea creando una estructura din´amica con 3 × 3 × 3 elementos, inicializarla, mostrar su contenido y liberar la memoria ocupada: tridimensional.c tridimensional.c 1 #include <stdio.h> 2 #include <stdlib.h> 3 4 int main(void) 5 { 6 int *** a; // Tres asteriscos: vector de vectores de vectores de enteros. 7 int i, j, k; 8 9 // Reserva de memoria 10 a = malloc(3*sizeof(int **)); 11 for (i=0; i<3; i++) { 12 a[i] = malloc(3*sizeof(int *)); 13 for (j=0; j<3; j++) 14 a[i][j] = malloc(3*sizeof(int)); 15 } 16 17 // Inicializaci´on 18 for (i=0; i<3; i++) 19 for (j=0; j<3; j++) 20 for (k=0; k<3; k++) 21 a[i][j][k] = i+j+k; 22 242 Introducci´on a la Programaci´on con C
  • 249. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica 23 // Impresi´on 24 for (i=0; i<3; i++) 25 for (j=0; j<3; j++) 26 for (k=0; k<3; k++) 27 printf ("%d %d %d: %dn", i, j, k, a[i][j][k]); 28 29 // Liberaci´on de memoria. 30 for (i=0; i<3; i++) { 31 for (j=0; j<3; j++) 32 free(a[i][j]); 33 free(a[i]); 34 } 35 free(a); 36 a = NULL; 37 38 return 0; 39 } En la siguiente figura se muestra el estado de la memoria tras la inicializaci´on de la matriz tridimensional: a 0 0 1 2 1 0 1 2 2 0 1 2 0 0 1 1 2 2 1 0 2 1 3 2 2 0 3 1 4 2 1 0 2 1 3 2 2 0 3 1 4 2 3 0 4 1 5 2 2 0 3 1 4 2 3 0 4 1 5 2 4 0 5 1 6 2 4.4. Redimensionamiento de la reserva de memoria Muchos programas no pueden determinar el tama˜no de sus vectores antes de empezar a trabajar con ellos. Por ejemplo, cuando se inicia la ejecuci´on de un programa que gestiona una agenda telef´onica no sabemos cu´antas entradas contendr´a finalmente. Podemos fijar un n´umero m´aximo de entradas y pedir memoria para ellas con malloc, pero entonces estaremos reproduciendo el problema que nos llev´o a presentar los vectores din´amicos. Afortunadamente, C permite que el tama˜no de un vector cuya memoria se ha solicitado previamente con malloc crezca en funci´on de las necesidades. Se usa para ello la funci´on realloc, cuyo prototipo es (similar a) ´este: stdlib.h 1 ... 2 void * realloc(void * puntero, int bytes); 3 ... Aqu´ı tienes un ejemplo de uso: Introducci´on a la Programaci´on con C 243
  • 250. 4.4 Redimensionamiento de la reserva de memoria 1 #include <stdlib.h> 2 3 int main(void) 4 { 5 int * a; 6 7 a = malloc(10 * sizeof(int)); // Se pide espacio para 10 enteros. 8 ... 9 a = realloc(a, 20 * sizeof(int)); // Ahora se ampl´ıa para que quepan 20. 10 ... 11 a = realloc(a, 5 * sizeof(int)); // Y ahora se reduce a s´olo 5 (los 5 primeros). 12 ... 13 free(a); 14 15 return 0; 16 } La funci´on realloc recibe como primer argumento el puntero que indica la zona de memoria que deseamos redimensionar y como segundo argumento, el n´umero de bytes que deseamos asignar ahora. La funci´on devuelve el puntero a la nueva zona de memoria. ¿Qu´e hace exactamente realloc? Depende de si se pide m´as o menos memoria de la que ya se tiene reservada: Si se pide m´as memoria, intenta primero ampliar la zona de memoria asignada. Si las posiciones de memoria que siguen a las que ocupa a en ese instante est´an libres, se las asigna a a, sin m´as. En caso contrario, solicita una nueva zona de memoria, copia el contenido de la vieja zona de memoria en la nueva, libera la vieja zona de memoria y nos devuelve el puntero a la nueva. Si se pide menos memoria, libera la que sobra en el bloque reservado. Un caso extremo consiste en llamar a realloc con una petici´on de 0 bytes. En tal caso, la llamada a realloc es equivalente a free. Al igual que malloc, si realloc no puede atender una petici´on, devuelve un puntero a NULL. Una cosa m´as: si se llama a realloc con el valor NULL como primer par´ametro, realloc se comporta como si se llamara a malloc. Como puedes imaginar, un uso constante de realloc puede ser una fuente de ineficiencia. Si tienes un vector que ocupa un 1 megabyte y usas realloc para que ocupe 1.1 megabyes, es probable que provoques una copia de 1 megabyte de datos de la zona vieja a la nueva. Es m´as, puede que ni siquiera tengas memoria suficiente para efectuar la nueva reserva, pues durante un instante (mientras se efect´ua la copia) estar´as usando 2.1 megabytes. Desarrollemos un ejemplo para ilustrar el concepto de reasignaci´on o redimensionamiento de memoria. Vamos a dise˜nar un m´odulo que permita crear diccionarios de palabras. Un diccionario es un vector de cadenas. Cuando creamos el diccionario, no sabemos cu´antas palabras albergar´a ni qu´e longitud tiene cada una de las palabras. Tendremos que usar, pues, memoria din´amica. Las palabras, una vez se introducen en el diccionario, no cambian de tama˜no, as´ı que bastar´a con usar malloc para gestionar sus reservas de memoria. Sin embargo, la talla de la lista de palabras s´ı var´ıa al a˜nadir palabras, as´ı que deberemos gestionarla con malloc/realloc. Empecemos por definir el tipo de datos para un diccionario. 1 struct Diccionario { 2 char ** palabra; 3 int palabras; 4 }; Aqu´ı tienes un ejemplo de diccionario que contiene 4 palabras: 3 2 1 0 t a c o 0 m a n o 0 d a d i v o s o 0 a n u a l 0 palabra palabras 4 244 Introducci´on a la Programaci´on con C
  • 251. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica Un diccionario vac´ıo no tiene palabra alguna ni memoria reservada. Esta funci´on crea un diccionario: 1 struct Diccionario crea_diccionario(void) 2 { 3 struct Diccionario d; 4 d.palabra = NULL; 5 d.palabras = 0; 6 return d; 7 } Ya podemos desarrollar la funci´on que inserta una palabra en el diccionario. Lo primero que har´a la funci´on es comprobar si la palabra ya est´a en el diccionario. En tal caso, no har´a nada: 1 void inserta_palabra_en_diccionario(struct Diccionario * d, char pal[]) 2 { 3 int i; 4 5 for (i=0; i<d->palabras; i++) 6 if (strcmp(d->palabra[i], pal)==0) 7 return; 8 ... 9 } Si la palabra no est´a, hemos de a˜nadirla: 1 void inserta_palabra_en_diccionario(struct Diccionario * d, char pal[]) 2 { 3 int i; 4 5 for (i=0; i<d->palabras; i++) 6 if (strcmp(d->palabra[i], pal)==0) 7 return; 8 9 d->palabra = realloc(d->palabra, (d->palabras+1) * sizeof(char *)); 10 d->palabra[d->palabras] = malloc((strlen(pal)+1) * sizeof(char)); 11 strcpy(d->palabra[d->palabras], pal); 12 d->palabras++; 13 } Podemos liberar la memoria ocupada por un diccionario cuando no lo necesitamos m´as llamando a esta otra funci´on: 1 void libera_diccionario(struct Diccionario * d) 2 { 3 int i; 4 5 if (d->palabra != NULL) { 6 for (i=0; i<d->palabras; i++) 7 free(d->palabra[i]); 8 free(d->palabra); 9 d->palabra = NULL; 10 d->palabras = 0; 11 } 12 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 239 Dise˜na una funci´on que devuelva cierto (valor 1) o falso (valor 0) en funci´on de si una palabra pertenece o no a un diccionario. · 240 Dise˜na una funci´on que borre una palabra del diccionario. · 241 Dise˜na una funci´on que muestre por pantalla todas la palabras del diccionario que empiezan por un prefijo dado (una cadena). Introducci´on a la Programaci´on con C 245
  • 252. 4.4 Redimensionamiento de la reserva de memoria No es lo mismo un puntero que un vector Aunque C permite considerarlos una misma cosa en muchos contextos, hay algunas di- ferencias entre un puntero a una serie de enteros, por ejemplo, y un vector de enteros. Consideremos un programa con estas declaraciones: 1 int vector[10]; 2 int escalar; 3 int * puntero; 4 int * otro_puntero; A los punteros debe asign´arseles expl´ıcitamente alg´un valor: • a la ((nada)): puntero = NULL; • a memoria reservada mediante malloc: puntero = malloc(5*sizeof(int)); • a la direcci´on de memoria de una variable escalar del tipo al que puede apuntar el puntero: puntero = &escalar; • a la direcci´on de memoria en la que empieza un vector: puntero = vector; • a la direcci´on de memoria de un elemento de un vector: puntero = &vector[2]; • a la direcci´on de memoria apuntada por otro puntero: puntero = otro_puntero; • a una direcci´on calculada mediante aritm´etica de punteros: por ejemplo, pun- tero = vector+2 es lo mismo que puntero = &vector[2]. Los vectores reservan memoria autom´aticamente, pero no puedes redimensionarlos. Es ilegal, por ejemplo, una sentencia como ´esta: vector = puntero. Eso s´ı, las funciones que admiten el paso de un vector v´ıa par´ametros, admiten tambi´en un puntero y viceversa. De ah´ı que se consideren equivalentes. Aunque suponga una simplificaci´on, puedes considerar que un vector es un puntero inmutable (de valor constante). · 242 Dise˜na una funci´on que muestre por pantalla todas la palabras del diccionario que acaban con un sufijo dado (una cadena). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La funci´on que determina si una palabra pertenece o no a un diccionario requiere tanto m´as tiempo cuanto mayor es el n´umero de palabras del diccionario. Es as´ı porque el diccionario est´a desordenado y, por tanto, la ´unica forma de estar seguros de que una palabra no est´a en el diccionario es recorrer todas y cada una de las palabras (si, por contra, la palabra est´a en el diccionario, no siempre es necesario recorrer el listado completo). Podemos mejorar el comportamiento de la rutina de b´usqueda si mantenemos el dicciona- rio siempre ordenado. Para ello hemos de modificar la funci´on de inserci´on de palabras en el diccionario: 1 void inserta_palabra_en_diccionario(struct Diccionario *d, char pal[]) 2 { 3 int i, j; 4 5 for (i=0; i<d->palabras; i++) { 6 if (strcmp(d->palabra[i], pal)==0) // Si ya est´a, no hay nada que hacer. 7 return; 8 if (strcmp(d->palabra[i], pal)>0) // Aqu´ı hemos encontrado su posici´on (orden alfab´etico) 9 break; 10 } 11 12 /* Si llegamos aqu´ı, la palabra no est´a y hay que insertarla en la posici´on i, desplazando 13 antes el resto de palabras. */ 15 16 /* Reservamos espacio en la lista de palabras para una m´as. */ 17 d->palabra = realloc(d->palabra, (d->palabras+1) * sizeof(char *)); 18 /* Desplazamos el resto de palabras para que haya un hueco en el vector. */ 19 for (j=d->palabras; j>i; j--) { 246 Introducci´on a la Programaci´on con C
  • 253. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica 20 d->palabra[j] = malloc(strlen(d->palabra[j-1])+1)*sizeof(char)); 21 strcpy(d->palabra[j], d->palabra[j-1]); 22 free(d->palabra[j-1]); 23 } 24 /* Y copiamos en su celda la nueva palabra */ 25 d->palabra[i] = malloc( (strlen(pal)+1) * sizeof(char) ); 26 strcpy(d->palabra[i], pal); 27 d->palabras++; 28 } ¡Buf! Las l´ıneas 20–22 no hacen m´as que asignar a una palabra el contenido de otra (la que ocupa la posici´on j recibe una copia del contenido de la que ocupa la posici´on j-1). ¿No hay una forma mejor de hacer eso mismo? S´ı. Transcribimos nuevamente las ´ultimas l´ıneas del programa, pero con una sola sentencia que sustituye a las l´ıneas 20–22: 18 ... 19 for (j=d->palabras; j>i; i--) 20 d->palabra[j] = d->palabra[j-1]; 21 /* Y copiamos en su celda la nueva palabra */ 22 d->palabra[i] = malloc( (strlen(pal)+1) * sizeof(char) ); 23 strcpy(d->palabra[i], pal); 24 d->palabras++; 25 } No est´a mal, pero ¡no hemos pedido ni liberado memoria din´amica! ¡Ni siquiera hemos usado strcpy, y eso que dijimos que hab´ıa que usar esa funci´on para asignar una cadena a otra. ¿C´omo es posible? Antes hemos de comentar qu´e significa una asignaci´on como ´esta: 1 d->palabra[j] = d->palabra[j-1]; Significa que d->palabra[j] apunta al mismo lugar al que apunta d->palabra[j-1]. ¿Por qu´e? Porque un puntero no es m´as que una direcci´on de memoria y asignar a un puntero el valor de otro hace que ambos contengan la misma direcci´on de memoria, es decir, que ambos apunten al mismo lugar. Veamos qu´e pasa estudiando un ejemplo. Imagina un diccionario en el que ya hemos insertado las palabras ((anual)), ((dadivoso)), ((mano)) y ((taco)) y que vamos a insertar ahora la palabra ((feliz)). Partimos, pues, de esta situaci´on: 3 2 1 0 t a c o 0 m a n o 0 d a d i v o s o 0 a n u a l 0 palabra palabras 4 Una vez hemos redimensionado el vector de palabras, tenemos esto: 4 3 2 1 0 t a c o 0 m a n o 0 d a d i v o s o 0 a n u a l 0 palabra palabras 4 La nueva palabra debe insertarse en la posici´on de ´ındice 2. El bucle ejecuta la asignaci´on d->palabra[j] = d->palabra[j-1] para j tomando los valores 4 y 3. Cuando se ejecuta la iteraci´on con j igual a 4, tenemos: Introducci´on a la Programaci´on con C 247
  • 254. 4.4 Redimensionamiento de la reserva de memoria 4 3 2 1 0 t a c o 0 m a n o 0 d a d i v o s o 0 a n u a l 0 palabra palabras 4 La ejecuci´on de la asignaci´on ha hecho que d->palabra[4] apunte al mismo lugar que d->palabra[3]. No hay problema alguno en que dos punteros apunten a un mismo bloque de memoria. En la siguiente iteraci´on pasamos a esta otra situaci´on: 4 3 2 1 0 t a c o 0 m a n o 0 d a d i v o s o 0 a n u a l 0 palabra palabras 4 Podemos reordenar gr´aficamente los elementos, para ver que, efectivamente, estamos haciendo hueco para la nueva palabra: 4 3 2 1 0 t a c o 0 m a n o 0 d a d i v o s o 0 a n u a l 0 palabra palabras 4 El bucle ha acabado. Ahora se pide memoria para el puntero d->palabra[i] (siendo i igual a 2). Se piden 6 bytes (((feliz)) tiene 5 caracteres m´as el terminador nulo): 4 3 2 1 0 t a c o 0 m a n o 0 d a d i v o s o 0 a n u a l 0 palabra palabras 4 Y, finalmente, se copia en d->palabra[i] el contenido de pal con la funci´on strcpy y se incrementa el valor de d->palabras: 4 3 2 1 0 t a c o 0 m a n o 0 f e l i z 0 d a d i v o s o 0 a n u a l 0 palabra palabras 5 248 Introducci´on a la Programaci´on con C
  • 255. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica Podemos ahora implementar una funci´on de b´usqueda de palabras m´as eficiente. Una pri- mera idea consiste en buscar desde el principio y parar cuando se encuentre la palabra buscada o cuando se encuentre una palabra mayor (alfab´eticamente) que la buscada. En este ´ultimo caso sabremos que la palabra no existe. Pero a´un hay una forma m´as eficiente de saber si una palabra est´a o no en una lista ordenada: mediante una b´usqueda dicot´omica. 1 int buscar_en_diccionario(struct Diccionario d, char pal[]) 2 { 3 int izquierda, centro, derecha; 4 5 izquierda = 0; 6 derecha = d.palabras; 7 while (izquierda < derecha) { 8 centro = (izquierda+derecha) / 2; 9 if (strcmp(pal, d.palabra[centro]) == 0) 10 return 1; 11 else if (strcmp(pal, d.palabra[centro]) < 0) 12 derecha = centro; 13 else 14 izquierda = centro+1; 15 } 16 return 0; 17 } Podemos hacer una peque˜na mejora para evitar el sobrecoste de llamar dos veces a la funci´on strcmp: 1 int buscar_en_diccionario(struct Diccionario d, char pal[]) 2 { 3 int izquierda, centro, derecha, comparacion ; 4 5 izquierda = 0; 6 derecha = d.palabras; 7 while (izquierda < derecha) { 8 centro = (izquierda+derecha) / 2; 9 comparacion = strcmp(pal, d.palabra[centro]); 10 if (comparacion == 0) 11 return 1; 12 else if (comparacion < 0) 13 derecha = centro; 14 else 15 izquierda = centro+1; 16 } 17 return 0; 18 } Juntemos todas las piezas y a˜nadamos una funci´on main que nos pida primero las palabras del diccionario y, a continuaci´on, nos pida palabras que buscar en ´el: diccionario.c diccionario.c 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 5 #define MAXLON 80 6 7 struct Diccionario { 8 char ** palabra; 9 int palabras; 10 }; 11 12 struct Diccionario crea_diccionario(void) 13 { 14 struct Diccionario d; Introducci´on a la Programaci´on con C 249
  • 256. 4.4 Redimensionamiento de la reserva de memoria 15 d.palabra = NULL; 16 d.palabras = 0; 17 return d; 18 } 19 20 void libera_diccionario(struct Diccionario * d) 21 { 22 int i; 23 24 if (d->palabra != NULL) { 25 for (i=0; i<d->palabras; i++) 26 free(d->palabra[i]); 27 free(d->palabra); 28 d->palabra = NULL; 29 d->palabras = 0; 30 } 31 } 32 33 void inserta_palabra_en_diccionario(struct Diccionario *d, char pal[]) 34 { 35 int i, j; 36 37 for (i=0; i<d->palabras; i++) { 38 if (strcmp(d->palabra[i], pal)==0) // Si ya est´a, no hay nada que hacer. 39 return; 40 if (strcmp(d->palabra[i], pal)>0) // Aqu´ı hemos encontrado su posici´on (orden alfab´etico) 41 break; 42 } 43 44 /* Si llegamos aqu´ı, la palabra no est´a y hay que insertarla en la posici´on i, desplazando 45 antes el resto de palabras. */ 47 48 /* Reservamos espacio en la lista de palabras para una m´as. */ 49 d->palabra = realloc(d->palabra, (d->palabras+1) * sizeof(char *)); 50 /* Desplazamos el resto de palabras para que haya un hueco en el vector. */ 51 for (j=d->palabras; j>i; j--) { 52 d->palabra[j] = malloc((strlen(d->palabra[j-1])+1)*sizeof(char)); 53 strcpy(d->palabra[j], d->palabra[j-1]); 54 free(d->palabra[j-1]); 55 } 56 /* Y copiamos en su celda la nueva palabra */ 57 d->palabra[i] = malloc( (strlen(pal)+1) * sizeof(char) ); 58 strcpy(d->palabra[i], pal); 59 d->palabras++; 60 } 61 62 int buscar_en_diccionario(struct Diccionario d, char pal[]) 63 { 64 int izquierda, centro, derecha, comparacion; 65 66 izquierda = 0; 67 derecha = d.palabras; 68 while (izquierda < derecha) { 69 centro = (izquierda+derecha) / 2; 70 comparacion = strcmp(pal, d.palabra[centro]); 71 if (comparacion == 0) 72 return 1; 73 else if (comparacion < 0) 74 derecha = centro; 75 else 76 izquierda = centro+1; 77 } 78 return 0; 250 Introducci´on a la Programaci´on con C
  • 257. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica 79 } 80 81 int main(void) 82 { 83 struct Diccionario mi_diccionario; 84 int num_palabras; 85 char linea[MAXLON+1]; 86 87 mi_diccionario = crea_diccionario(); 88 89 printf (" ? Cu´antas palabras tendr´a el diccionario?: "); 90 gets(linea); sscanf (linea, "%d", &num_palabras); 91 while (mi_diccionario.palabras != num_palabras) { 92 printf ("Palabra %d: ", mi_diccionario.palabras+1); 93 gets(linea); 94 inserta_palabra_en_diccionario(&mi_diccionario, linea); 95 } 96 97 do { 98 printf (" ? Qu´e palabra busco? (pulsa retorno para acabar): "); 99 gets(linea); 100 if (strlen(linea) > 0) { 101 if (buscar_en_diccionario(mi_diccionario, linea)) 102 printf ("S´ı que est´a.n"); 103 else 104 printf ("No est´a.n"); 105 } 106 } while(strlen(linea) > 0); 107 108 libera_diccionario(&mi_diccionario); 109 110 return 0; 111 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 243 ¿Cu´antas comparaciones se hacen en el peor de los casos en la b´usqueda dicot´omica de una palabra cualquiera en un diccionario con 8 palabras? ¿Y en un diccionario con 16 palabras? ¿Y en uno con 32? ¿Y en uno con 1024? ¿Y en uno con 1048576? (Nota: el valor 1048576 es igual a 220 .) · 244 Al insertar una nueva palabra en un diccionario hemos de comprobar si exist´ıa previa- mente y, si es una palabra nueva, averiguar en qu´e posici´on hay que insertarla. En la ´ultima versi´on presentada, esa b´usqueda se efect´ua recorriendo el diccionario palabra a palabra. Mo- dif´ıcala para que esa b´usqueda sea dicot´omica. · 245 Dise˜na una funci´on que funda dos diccionarios ordenados en uno s´olo (tambi´en orde- nado) que se devolver´a como resultado. La fusi´on se har´a de modo que las palabras que est´an repetidas en ambos diccionarios aparezcan una s´ola vez en el diccionario final. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.4.1. Una aplicaci´on: una agenda telef´onica Vamos a desarrollar un ejemplo completo: una agenda telef´onica. Utilizaremos vectores din´amicos en diferentes estructuras de datos del programa. Por ejemplo, el nombre de las personas registra- das en la agenda y sus n´umeros de tel´efonos consumir´an exactamente la memoria que necesitan, sin desperdiciar un s´olo byte. Tambi´en el n´umero de entradas en la agenda se adaptar´a a las necesidades reales de cada ejecuci´on. El nombre de una persona no cambia durante la ejecu- ci´on del programa, as´ı que no necesitar´a redimensionamiento, pero la cantidad de n´umeros de tel´efono de una misma persona s´ı (se pueden a˜nadir n´umeros de tel´efono a la entrada de una persona que ya ha sido dada de alta). Gestionaremos esa memoria con redimensionamiento, del mismo modo que usaremos redimensionamiento para gestionar el vector de entradas de la agenda: gastaremos exactamente la memoria que necesitemos para almacenar la informaci´on. Introducci´on a la Programaci´on con C 251
  • 258. 4.4 Redimensionamiento de la reserva de memoria Aqu´ı tienes un texto con el tipo de informaci´on que deseamos almacenar: Pepe P´erez 96 111111 96 369246 Ana Garc´ıa 964 321654 964 987654 964 001122 Juan Gil Mar´ıa Paz 964 123456 Para que te hagas una idea del montaje, te mostramos la representaci´on gr´afica de las estructuras de datos con las que representamos la agenda del ejemplo: persona personas 4 nombre telefono telefonos 2 0 nombre telefono telefonos 3 1 nombre telefono telefonos 0 2 nombre telefono telefonos 1 3 P 0 e 1 p 2 e 3 4 P 5 ´e 6 r 7 e 8 z 9 0 10 A 0 n 1 a 2 3 G 4 a 5 r 6 c 7 ´ı 8 a 9 0 10 J 0 u 1 a 2 n 3 4 G 5 i 6 l 7 0 8 M 0 a 1 r 2 ´ı 3 a 4 5 P 6 a 7 z 8 0 9 0 0 1 2 3 4 5 6 7 8 9 10 9 6 4 1 2 3 4 5 6 0 0 0 1 2 3 4 5 6 7 8 9 10 1 0 1 2 3 4 5 6 7 8 9 10 2 0 1 2 3 4 5 6 7 8 9 10 9 6 4 3 2 1 6 5 4 0 9 6 4 9 8 7 6 5 4 0 9 6 4 0 0 1 1 2 2 0 0 0 1 2 3 4 5 6 7 8 9 1 0 1 2 3 4 5 6 7 8 9 9 6 1 1 1 1 1 1 0 9 6 3 6 9 2 4 6 0 Empezaremos proporcionando ((soporte)) para el tipo de datos ((entrada)): un nombre y un listado de tel´efonos (un vector din´amico). 1 #include <stdio.h> 2 #include <stdlib.h> 3 4 /************************************************************************ 5 * Entradas 6 ************************************************************************/ 8 9 struct Entrada { 10 char * nombre; // Nombre de la persona. 11 char ** telefono; // Vector din´amico de n´umeros de tel´efono. 12 int telefonos; // N´umero de elementos en el anterior vector. 13 }; 14 15 void crea_entrada(struct Entrada * e, char * nombre) 16 /* Inicializa una entrada con un nombre. De momento, la lista de tel´efonos se pone a NULL. */ 17 { 18 /* Reservamos memoria para el nombre y efectuamos la asignaci´on. */ 19 e->nombre = malloc((strlen(nombre)+1)*sizeof(char)); 20 strcpy(e->nombre, nombre); 252 Introducci´on a la Programaci´on con C
  • 259. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica 21 22 e->telefono = NULL; 23 e->telefonos = 0; 24 } 25 26 void anyadir_telefono_a_entrada(struct Entrada * e, char * telefono) 27 { 28 e->telefono = realloc(e->telefono, (e->telefonos+1)*sizeof(char *)); 29 e->telefono[e->telefonos] = malloc((strlen(telefono)+1)*sizeof(char)); 30 strcpy(e->telefono[e->telefonos], telefono); 31 e->telefonos++; 32 } 33 34 void muestra_entrada(struct Entrada * e) 35 // Podr´ıamos haber pasado e por valor, pero resulta m´as eficiente (y no mucho m´as 36 // inc´omodo) hacerlo por referencia: pasamos as´ı s´olo 4 bytes en lugar de 12. 37 { 38 int i; 39 40 printf ("Nombre: %sn", e->nombre); 41 for(i=0; i<e->telefonos; i++) 42 printf (" Tel´efono %d: %sn", i+1, e->telefono[i]); 43 } 44 45 void libera_entrada(struct Entrada * e) 46 { 47 int i; 48 49 free(e->nombre); 50 for (i=0; i<e->telefonos; i++) 51 free(e->telefono[i]); 52 free(e->telefono); 53 54 e->nombre = NULL; 55 e->telefono = NULL; 56 e->telefonos = 0; 57 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 246 Modifica anyadir_telefono_a_entrada para que compruebe si el tel´efono ya hab´ıa sido dado de alta. En tal caso, la funci´on dejar´a intacta la lista de tel´efonos de esa entrada. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ya tenemos resuelta la gesti´on de entradas. Ocup´emonos ahora del tipo agenda y de su gesti´on. 1 /************************************************************************ 2 * Agenda 3 ************************************************************************/ 5 6 struct Agenda { 7 struct Entrada * persona; /* Vector de entradas */ 8 int personas; /* N´umero de entradas en el vector */ 9 }; 10 11 struct Agenda crea_agenda(void) 12 { 13 struct Agenda a; 14 15 a.persona = NULL; 16 a.personas = 0; 17 return a; 18 } 19 Introducci´on a la Programaci´on con C 253
  • 260. 4.4 Redimensionamiento de la reserva de memoria 20 void anyadir_persona(struct Agenda * a, char * nombre) 21 { 22 int i; 23 24 /* Averiguar si ya tenemos una persona con ese nombre */ 25 for (i=0; i<a->personas; i++) 26 if (strcmp(a->persona[i].nombre, nombre) == 0) 27 return; 28 29 /* Si llegamos aqu´ı, es porque no ten´ıamos registrada a esa persona. */ 30 a->persona = realloc(a->persona, (a->personas+1) * sizeof(struct Entrada)); 31 crea_entrada(&a->persona[a->personas], nombre); 32 a->personas++; 33 } 34 35 void muestra_agenda(struct Agenda * a) 36 // Pasamos a as´ı por eficiencia. 37 { 38 int i; 39 40 for (i=0; i<a->personas; i++) 41 muestra_entrada(&a->persona[i]); 42 } 43 44 struct Entrada * buscar_entrada_por_nombre(struct Agenda * a, char * nombre) 45 { 46 int i; 47 48 for (i=0; i<a->personas; i++) 49 if (strcmp(a->persona[i].nombre, nombre)==0) 50 return & a->persona[i]; 51 52 /* Si llegamos aqu´ı, no lo hemos encontrado. Devolvemos NULL para indicar que no 53 ((conocemos)) a esa persona. */ 55 return NULL; 56 } 57 58 void libera_agenda(struct Agenda * a) 59 { 60 int i; 61 62 for (i=0; i<a->personas; i++) 63 libera_entrada(&a->persona[i]); 64 free(a->persona); 65 a->persona = NULL; 66 a->personas = 0; 67 } F´ıjate en el prototipo de buscar_entrada_por_nombre: devuelve un puntero a un dato de tipo struct Entrada. Es un truco bastante utilizado. Si no existe una persona con el nombre indicado, se devuelve un puntero a NULL, y si existe, un puntero a esa persona. Ello nos permite, por ejemplo, mostrarla a continuaci´on llamando a la funci´on que muestra ((entradas)), pues espera un puntero a un struct Entrada. Ahora ver´as c´omo lo hacemos en el programa principal. Ya podemos escribir el programa principal. agenda.c agenda.c 1 #include <stdio.h> 2 #include <stdlib.h> 3 4 #define MAXLON_NOMBRE 200 5 #define MAXLON_TELEFONO 40 6 #define MAXLON_LINEA 80 7 8 enum { Ver=1, AltaPersona, AnyadirTelefono, Buscar, Salir }; 254 Introducci´on a la Programaci´on con C
  • 261. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica Un lenguaje para cada tarea Acabas de ver que el tratamiento de cadenas en C es bastante primitivo y nos obliga a estar pendientes de numerosos detalles. La memoria que ocupa cada cadena ha de ser expl´ıcitamente controlada por el programador y las operaciones que podemos hacer con ellas son, en principio, primitivas. Python, por contra, libera al programador de innumerables preocupaciones cuando trabaja con objetos como las cadenas o los vectores din´amicos (sus listas). Adem´as, ofrece ((de f´abrica)) numerosas utilidades para manipular cadenas (cortes, funciones del m´odulo string, etc.) y listas (m´etodo append, sentencia del, cortes, ´ındices negativos, etc.). ¿Por qu´e no usamos siempre Python? Por eficiencia. C permite dise˜nar, por regla general, programas mucho m´as eficientes que sus equivalentes en Python. La mayor flexibilidad de Python tiene un precio. Antes de programar una aplicaci´on, hemos de preguntarnos si la eficiencia es un factor cr´ıtico. Un programa con formularios (con un interfaz gr´afico) y/o accesos a una base de datos funcionar´a probablemente igual de r´apido en C que en Python, ya que el cuello de botella de la ejecuci´on lo introduce el propio usuario con su (lenta) velocidad de introducci´on de datos y/o el sistema de base de datos al acceder a la informaci´on. En tal caso, parece sensato escoger el lenguaje m´as flexible, el que permita desarrollar la aplicaci´on con mayor facilidad. Un programa de c´alculo matricial, un sistema de adquisici´on de im´agenes para una c´amara de v´ıdeo digital, etc. han de ejecutarse a una velocidad que (probablemente) excluya a Python como lenguaje para la implementaci´on. Hay lenguajes de programaci´on que combinan la eficiencia de C con parte de la flexi- bilidad de Python y pueden suponer una buena soluci´on de compromiso en muchos casos. Entre ellos hemos de destacar el lenguaje C++, que estudiar´as el pr´oximo curso. Y hay una opci´on adicional: implementar en el lenguaje eficiente las rutinas de c´alculo pesadas y usarlas desde un lenguaje de programaci´on flexible. Python ofrece un interfaz que permite el uso de m´odulos escritos en C o C++. Su uso no es trivial, pero hay herramientas como ((SWIG)) o ((Boost.Python)) que simplifican enormemente estas tareas. 9 . . . 133 134 /************************************************************************ 135 * Programa principal 136 ************************************************************************/ 138 139 int main(void) 140 { 141 struct Agenda miagenda; 142 struct Entrada * encontrada; 143 int opcion; 144 char nombre[MAXLON_NOMBRE+1]; 145 char telefono[MAXLON_TELEFONO+1]; 146 char linea[MAXLON_LINEA+1]; 147 148 miagenda = crea_agenda(); 149 150 do { 151 printf ("Men´u:n"); 152 printf ("1) Ver contenido completo de la agenda.n"); 153 printf ("2) Dar de alta una persona.n"); 154 printf ("3) A~nadir un tel´efono.n"); 155 printf ("4) Buscar tel´efonos de una persona.n"); 156 printf ("5) Salir.n"); 157 printf ("Opci´on: "); 158 gets(linea); sscanf (linea, "%d", &opcion); 159 160 switch(opcion) { 161 162 case Ver: 163 muestra_agenda(&miagenda); Introducci´on a la Programaci´on con C 255
  • 262. 4.5 Introducci´on a la gesti´on de registros enlazados 164 break; 165 166 case AltaPersona: 167 printf ("Nombre: "); 168 gets(nombre); 169 anyadir_persona(&miagenda, nombre); 170 break; 171 172 case AnyadirTelefono: 173 printf ("Nombre: "); 174 gets(nombre); 175 encontrada = buscar_entrada_por_nombre(&miagenda, nombre); 176 if (encontrada == NULL) { 177 printf ("No hay nadie llamado %s en la agenda.n", nombre); 178 printf ("Por favor, d´e antes de alta a %s.n", nombre); 179 } 180 else { 181 printf ("Telefono: "); 182 gets(telefono); 183 anyadir_telefono_a_entrada(encontrada, telefono); 184 } 185 break; 186 187 case Buscar: 188 printf ("Nombre: "); 189 gets(nombre); 190 encontrada = buscar_entrada_por_nombre(&miagenda, nombre); 191 if (encontrada == NULL) 192 printf ("No hay nadie llamado %s en la agenda.n", nombre); 193 else 194 muestra_entrada(encontrada); 195 break; 196 } 197 } while (opcion != Salir); 198 199 libera_agenda(&miagenda); 200 201 return 0; 202 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 247 Dise˜na una funci´on que permita eliminar una entrada de la agenda a partir del nombre de una persona. · 248 La agenda, tal y como la hemos implementado, est´a desordenada. Modifica el programa para que est´e siempre ordenada. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.5. Introducci´on a la gesti´on de registros enlazados Hemos aprendido a crear vectores din´amicos. Podemos crear secuencias de elementos de cual- quier tama˜no, aunque hemos visto que usar realloc para adaptar el n´umero de elementos re- servados a las necesidades de la aplicaci´on es una posible fuente de ineficiencia, pues puede provocar la copia de grandes cantidades de memoria. En esta secci´on aprenderemos a crear listas con registros enlazados. Este tipo de listas ajustan su consumo de memoria al tama˜no de la secuencia de datos que almacenan sin necesidad de llamar a realloc. Una lista enlazada es una secuencia de registros unidos por punteros de manera que cada registro contiene un valor y nos indica cu´al es el siguiente registro. As´ı pues, cada registro consta de uno o m´as campos con datos y un puntero: el que apunta al siguiente registro. El ´ultimo registro de la secuencia apunta a. . . nada. Aparte, un ((puntero maestro)) apunta al primero de los registros de la secuencia. F´ıjate en este gr´afico para ir captando la idea: 256 Introducci´on a la Programaci´on con C
  • 263. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica Redimensionamiento con holgura Estamos utilizando realloc para aumentar de celda en celda la reserva de memoria de los vectores que necesitan crecer. Este tipo de redimensionamientos, tan ajustados a las ne- cesidades exactas de cada momento, nos puede pasar factura: cada operaci´on realloc es potencialmente lenta, pues ya sabes que puede disparar la copia de un bloque de memoria a otro. Una t´ecnica para paliar este problema consiste en crecer varias celdas cada vez que nos quedamos cortos de memoria en un vector. Un campo adicional en el registro, llam´emosle capacidad, se usa para indicar cu´antas celdas tiene reservadas un vector y otro campo, digamos, talla, indica cu´antas de dichas celdas est´an realmente ocupadas. Por ejemplo, este vector, en el que s´olo estamos ocupando de momento tres celdas (las marcadas en negro), tendr´ıa talla igual a 3 y capacidad igual a 5: a 0 1 2 3 4 Cada vez que necesitamos escribir un nuevo dato en una celda adicional, comprobamos si talla es menor o igual que capacidad. En tal caso, no hace falta redimensionar el vector, basta con incrementar el valor de talla. Pero en caso contrario, nos curamos en salud y redimensionamos pidiendo memoria para, pongamos, 10 celdas m´as (y, consecuentemente, incrementamos el valor de capacidad en 10 unidades). De este modo reducimos el n´umero de llamadas a realloc a una d´ecima parte. Incrementar un n´umero fijo de celdas no es la ´unica estrategia posible. Otra aproximaci´on consiste en duplicar la capacidad cada vez que se precisa agrandar el vector. De este modo, el n´umero de llamadas a realloc es proporcional al logaritmo en base 2 del n´umero de celdas del vector. 1 struct VDH { // vector Din´amico con Holgura (VDH) 2 int * dato; 3 int talla, capacidad; 4 }; 5 6 struct VDH crea_VDH (void) { 7 struct VDH vdh; 8 9 vdh.dato = malloc(1*sizeof(int)); vdh.talla = 0; vdh.capacidad = 1; 10 return vdh; 11 } 12 13 void anyade_dato(struct VDH * vdh, int undato) 14 { 15 if (vdh->talla == vdh->capacidad) { 16 vdh->capacidad *= 2; 17 vdh->dato = realloc(vdh->dato, vdh->capacidad * sizeof(int)); 18 } 19 vdh->dato[vdh->talla++] = undato; 20 } 21 22 void elimina_ultimo(struct VDH * vdh) 23 { 24 if (vdh->talla < vdh->capacidad/2 && vdh->capacidad > 0) { 25 vdh->capacidad /= 2; 26 vdh->dato = realloc(vdh->dato, vdh->capacidad * sizeof(int)); 27 } 28 vdh->talla--; 29 } Ciertamente, esta aproximaci´on ((desperdicia)) memoria, pero la cantidad de memoria desperdiciada puede resultar tolerable para nuestra aplicaci´on. Python usa internamente una variante de esta t´ecnica cuando modificamos la talla de una lista con, por ejemplo, el m´etodo append (no duplica la memoria reservada, sino que la aumenta en cierta cantidad constante). Introducci´on a la Programaci´on con C 257
  • 264. 4.5 Introducci´on a la gesti´on de registros enlazados lista 3 info sig 8 info sig 2 info sig Conceptualmente, es lo mismo que se ilustra en este gr´afico: lista 3 0 8 1 2 2 Pero s´olo conceptualmente. En la implementaci´on, el nivel de complejidad al que nos enfren- tamos es mucho mayor. Eso s´ı, a cambio ganaremos en flexibilidad y podremos ofrecer ver- siones eficientes de ciertas operaciones sobre listas de valores que implementadas con vectores din´amicos ser´ıan muy costosas. Por otra parte, aprender estas t´ecnicas supone una inversi´on a largo plazo: muchas estructuras din´amicas que estudiar´as en cursos de Estructuras de Datos se basan en el uso de registros enlazados y, aunque mucho m´as complejas que las simples secuencias de valores, usan las mismas t´ecnicas b´asicas de gesti´on de memoria que aprender´as ahora. Para ir aprendiendo a usar estas t´ecnicas, gestionaremos ahora una lista con registros en- lazados. La manejaremos directamente, desde un programa principal, y nos centraremos en la realizaci´on de una serie de acciones que parten de un estado de la lista y la dejan en otro estado diferente. Ilustraremos cada una de las acciones mostrando con todo detalle qu´e ocurre paso a paso. En el siguiente apartado ((encapsularemos)) cada acci´on elemental (a˜nadir elemento, borrar elemento, etc.) en una funci´on independiente. 4.5.1. Definici´on y creaci´on de la lista Vamos a crear una lista de enteros. Empezamos por definir el tipo de los registros que componen la lista: 1 struct Nodo { 2 int info; 3 struct Nodo * sig; 4 }; Un registro de tipo struct Nodo consta de dos elementos: un entero llamado info, que es la informaci´on que realmente nos importa, y un puntero a un elemento que es. . . ¡otro struct Nodo! (Observa que hay cierto nivel de recursi´on o autoreferencia en la definici´on: un struct Nodo contiene un puntero a un struct Nodo. Por esta raz´on, las estructuras que vamos a estudiar se denominan a veces estructuras recursivas.) Si quisi´eramos manejar una lista de puntos en el plano, tendr´ıamos dos opciones: 1. definir un registro con varios campos para la informaci´on relevante: 1 struct Nodo { 2 float x; 3 float y; 4 struct Nodo * sig; 5 }; lista 1.1 7.1 x y sig 0.2 0.1 x y sig 3.7 2.1 x y sig 2. definir un tipo adicional y utilizar un ´unico campo de dicho tipo: 1 struct Punto { 2 float x; 3 float y; 4 }; 5 6 struct Nodo { 7 struct Punto info; 8 struct Nodo * sig; 9 }; 258 Introducci´on a la Programaci´on con C
  • 265. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica lista x 1.1 y 7.1 info sig x 0.2 y 0.1 info sig x 3.7 y 2.1 info sig Cualquiera de las dos opciones es v´alida, si bien la segunda es m´as elegante. Volvamos al estudio de nuestra lista de enteros. Creemos ahora el ((puntero maestro)), aqu´el en el que empieza la lista de enteros: 1 int main(void) 2 { 3 struct Nodo * lista; 4 5 ... No es m´as que un puntero a un elemento de tipo struct Nodo. Inicialmente, la lista est´a vac´ıa. Hemos de indicarlo expl´ıcitamente as´ı: 1 int main(void) 2 { 3 struct Nodo * lista = NULL; 4 5 ... Tenemos ahora esta situaci´on: lista O sea, lista no contiene nada, est´a vac´ıa. 4.5.2. Adici´on de nodos (por cabeza) Empezaremos a˜nadiendo un nodo a la lista. Nuestro objetivo es pasar de la lista anterior a esta otra: lista 8 info sig ¿C´omo creamos el nuevo registro? Con malloc: 1 int main(void) 2 { 3 struct Nodo * lista = NULL; 4 5 lista = malloc( sizeof(struct Nodo) ); 6 ... ´Este es el resultado: lista info sig Ya tenemos el primer nodo de la lista, pero sus campos a´un no tienen los valores que deben tener finalmente. Lo hemos representado gr´aficamente dejando el campo info en blanco y sin poner una flecha que salga del campo sig. Por una parte, el campo info deber´ıa contener el valor 8, y por otra, el campo sig deber´ıa apuntar a NULL: 1 int main(void) 2 { 3 struct Nodo * lista = NULL; 4 5 lista = malloc( sizeof(struct Nodo) ); 6 lista->info = 8; 7 lista->sig = NULL; 8 ... Introducci´on a la Programaci´on con C 259
  • 266. 4.5 Introducci´on a la gesti´on de registros enlazados No debe sorprenderte el uso del operador -> en las asignaciones a campos del registro. La variable lista es de tipo struct Nodo *, es decir, es un puntero, y el operador -> permite acceder al campo de un registro apuntado por un puntero. He aqu´ı el resultado: lista 8 info sig Ya tenemos una lista con un ´unico elemento. Vamos a a˜nadir un nuevo nodo a la lista, uno que contenga el valor 3 y que ubicaremos justo al principio de la lista, delante del nodo que contiene el valor 8. O sea, partimos de esta situaci´on: lista 8 info sig y queremos llegar a esta otra: lista 3 info sig 8 info sig En primer lugar, hemos de crear un nuevo nodo al que deber´a apuntar lista. El campo sig del nuevo nodo, por su parte, deber´ıa apuntar al nodo que contiene el valor 8. Empecemos por la petici´on de un nuevo nodo que, ya que debe ser apuntado por lista, podemos pedir y rellenar as´ı: 1 int main(void) 2 { 3 struct Nodo * lista = NULL; 4 5 ... 6 lista = malloc( sizeof(struct Nodo) ); 7 lista->info = 3; 8 lista->sig = ???; // No sabemos c´omo expresar esta asignaci´on. 9 ... ¡Algo ha ido mal! ¿C´omo podemos asignar a lista->sig la direcci´on del siguiente nodo con valor 8? La situaci´on en la que nos encontramos se puede representar as´ı: lista 3 info sig 8 info sig ¡No somos capaces de acceder al nodo que contiene el valor 8! Es lo que denominamos una p´erdida de referencia, un grave error en nuestro programa que nos imposibilita seguir construyendo la lista. Si no podemos acceder a un bloque de memoria que hemos pedido con malloc, tampoco podremos liberarlo luego con free. Cuando se produce una p´erdida de referencia hay, pues, una fuga de memoria: pedimos memoria al ordenador y no somos capaces de liberarla cuando dejamos de necesitarla. Un programa con fugas de memoria corre el riesgo de consumir toda la memoria disponible en el ordenador. Hemos de estar siempre atentos para evitar p´erdidas de referencia. Es uno de los mayores peligros del trabajo con memoria din´amica. ¿C´omo podemos evitar la p´erdida de referencia? Muy f´acil: con un puntero auxiliar. 1 int main(void) 2 { 3 struct Nodo * lista = NULL, * aux ; 4 5 ... 6 aux = lista ; 7 lista = malloc( sizeof(struct Nodo) ); 8 lista->info = 3; 9 lista->sig = aux ; 10 ... 260 Introducci´on a la Programaci´on con C
  • 267. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica La declaraci´on de la l´ınea 3 es curiosa. Cuando declaras dos o m´as punteros en una sola l´ınea, has de poner el asterisco delante del identificador de cada puntero. En una l´ınea de declaraci´on que empieza por la palabra int puedes declarar punteros a enteros y enteros, seg´un precedas los respectivos identificadores con asterisco o no. Deteng´amonos un momento para considerar el estado de la memoria justo despu´es de ejecutarse la l´ınea 6, que reza ((aux = lista)): aux lista 8 info sig El efecto de la l´ınea 6 es que tanto aux como lista apuntan al mismo registro. La asignaci´on de un puntero a otro hace que ambos apunten al mismo elemento. Recuerda que un puntero no es m´as que una direcci´on de memoria y que copiar un puntero a otro hace que ambos contengan la misma direcci´on de memoria, es decir, que ambos apunten al mismo lugar. Sigamos con nuestra traza. Veamos c´omo queda la memoria justo despu´es de ejecutar la l´ınea 7, que dice ((lista = malloc(sizeof(struct Nodo)))): aux lista info sig 8 info sig La l´ınea 8, que dice ((lista->info = 3)), asigna al campo info del nuevo nodo (apuntado por lista) el valor 3: aux lista 3 info sig 8 info sig La lista a´un no est´a completa, pero observa que no hemos perdido la referencia al ´ultimo fragmento de la lista. El puntero aux la mantiene. Nos queda por ejecutar la l´ınea 9, que efect´ua la asignaci´on ((lista->sig = aux)) y enlaza as´ı el campo sig del primer nodo con el segundo nodo, el apuntado por aux. Tras ejecutarla tenemos: aux lista 3 info sig 8 info sig ¡Perfecto! ¿Seguro? ¿Y qu´e hace aux apuntando a´un a la lista? La verdad, nos da igual. Lo importante es que los nodos que hay enlazados desde lista formen la lista que quer´ıamos construir. No importa c´omo quedan los punteros auxiliares: una vez han desempe˜nado su funci´on en la construcci´on de la lista, son sup´erfluos. Si te quedas m´as tranquilo, puedes a˜nadir una l´ınea con aux = NULL al final del programa para que aux no quede apuntando a un nodo de la lista, pero, repetimos, es innecesario. 4.5.3. Adici´on de un nodo (por cola) Marqu´emonos un nuevo objetivo. Intentemos a˜nadir un nuevo nodo al final de la lista. Es decir, partiendo de la ´ultima lista, intentemos obtener esta otra: lista 3 info sig 8 info sig 2 info sig ¿Qu´e hemos de hacer? Para empezar, pedir un nuevo nodo, s´olo que esta vez no estar´a apuntado por lista, sino por el que hasta ahora era el ´ultimo nodo de la lista. De momento, lo mantendremos apuntado por un puntero auxiliar. Despu´es, accederemos de alg´un modo al campo sig del ´ultimo nodo de la lista (el que tiene valor 8) y haremos que apunte al nuevo nodo. Finalmente, haremos que el nuevo nodo contenga el valor 2 y que tenga como siguiente nodo a NULL. Intent´emoslo: Introducci´on a la Programaci´on con C 261
  • 268. 4.5 Introducci´on a la gesti´on de registros enlazados 1 int main(void) 2 { 3 struct Nodo * lista = NULL, * aux; 4 5 ... 6 aux = malloc( sizeof(struct Nodo) ); 7 lista->sig->sig = aux ; 8 aux->info = 2; 9 aux->sig = NULL; 10 11 return 0; 12 } Veamos c´omo queda la memoria paso a paso. Tras ejecutar la l´ınea 6 tenemos: aux info sig lista 3 info sig 8 info sig O sea, la lista que ((cuelga)) de lista sigue igual, pero ahora aux apunta a un nuevo nodo. Pasemos a estudiar la l´ınea 7, que parece complicada porque contiene varias aplicaciones del operador ->. Esa l´ınea reza as´ı: lista->sig->sig = aux. Vamos a ver qu´e significa ley´endola de izquierda a derecha. Si lista es un puntero, y lista->sig es el campo sig del primer nodo, que es otro puntero, entonces lista->sig->sig es el campo sig del segundo nodo, que es otro puntero. Si a ese puntero le asignamos aux, el campo sig del segundo nodo apunta a donde apuntar´a aux. Aqu´ı tienes el resultado: aux info sig lista 3 info sig 8 info sig A´un no hemos acabado. Una vez hayamos ejecutado las l´ıneas 8 y 9, el trabajo estar´a completo: aux 2 info sig lista 3 info sig 8 info sig ¿Y es ´eso lo que busc´abamos? S´ı. Reordenemos gr´aficamente los diferentes componentes para que su disposici´on en la imagen se asemeje m´as a lo que esper´abamos: aux lista 3 info sig 8 info sig 2 info sig Ahora queda m´as claro que, efectivamente, hemos conseguido el objetivo. Esta figura y la anterior son absolutamente equivalentes. A´un hay algo en nuestro programa poco elegante: la asignaci´on ((lista->sig->sig = aux)) es complicada de entender y da pie a un m´etodo de adici´on por el final muy poco ((extensible)). ¿Qu´e queremos decir con esto ´ultimo? Que si ahora queremos a˜nadir a la lista de 3 nodos un cuarto nodo, tendremos que hacer ((lista->sig->sig->sig = aux)). Y si quisi´eramos a˜nadir un quinto, ((lista->sig->sig->sig->sig = aux)) Imagina que la lista tiene 100 o 200 elementos. ¡Menuda complicaci´on proceder as´ı para a˜nadir por el final! ¿No podemos expresar la idea ((a˜nadir por el final)) de un modo m´as elegante y general? S´ı. Podemos hacer lo siguiente: 262 Introducci´on a la Programaci´on con C
  • 269. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica 1. buscar el ´ultimo elemento con un bucle y mantenerlo referenciado con un puntero auxiliar, digamos aux; aux lista 3 info sig 8 info sig 2. pedir un nodo nuevo y mantenerlo apuntado con otro puntero auxiliar, digamos nuevo; aux lista nuevo info sig 3 info sig 8 info sig 3. escribir en el nodo apuntado por nuevo el nuevo dato y hacer que su campo sig apunte a NULL; aux lista nuevo 2 info sig 3 info sig 8 info sig 4. hacer que el nodo apuntado por aux tenga como siguiente nodo al nodo apuntado por nuevo. aux lista nuevo 2 info sig 3 info sig 8 info sig Lo que es equivalente a este otro gr´afico en el que, sencillamente, hemos reorganizado la disposici´on de los diferentes elementos: aux lista nuevo 3 info sig 8 info sig 2 info sig Modifiquemos el ´ultimo programa para expresar esta idea: 1 int main(void) 2 { 3 struct Nodo * lista = NULL, * aux, * nuevo ; 4 5 ... 6 aux = lista; 7 while (aux->sig != NULL) 8 aux = aux->sig; 9 nuevo = malloc( sizeof(struct Nodo) ); 10 nuevo->info = 2; Introducci´on a la Programaci´on con C 263
  • 270. 4.5 Introducci´on a la gesti´on de registros enlazados 11 nuevo->sig = NULL; 12 aux->sig = nuevo ; 13 14 return 0; 15 } La inicializaci´on y el bucle de las l´ıneas 6–8 buscan al ´ultimo nodo de la lista y lo mantienen apuntado con aux. El ´ultimo nodo se distingue porque al llegar a ´el, aux->sig es NULL, de ah´ı la condici´on del bucle. No importa cu´an larga sea la lista: tanto si tiene 1 elemento como si tiene 200, aux acaba apuntando al ´ultimo de ellos.5 Si partimos de una lista con dos elementos, ´este es el resultado de ejecutar el bucle: aux lista nuevo 3 info sig 8 info sig Las l´ıneas 9–11 dejan el estado de la memoria as´ı: aux lista nuevo 2 info sig 3 info sig 8 info sig Finalmente, la l´ınea 12 completa la adici´on del nodo: aux lista nuevo 2 info sig 3 info sig 8 info sig Y ya est´a. Eso es lo que busc´abamos. La inicializaci´on y el bucle de las l´ıneas 6–8 se pueden expresar en C de una forma mucho m´as compacta usando una estructura for: 1 int main(void) 2 { 3 struct Nodo * lista = NULL, * aux, * nuevo; 4 5 ... 6 for (aux = lista; aux->sig != NULL; aux = aux->sig) ; 7 nuevo = malloc( sizeof(struct Nodo) ); 8 nuevo->info = 2; 9 nuevo->sig = NULL; 10 aux->sig = nuevo; 11 12 return 0; 13 } Observa que el punto y coma que aparece al final del bucle for hace que no tenga sentencia alguna en su bloque: 1 for (aux = lista; aux->sig != NULL; aux = aux->sig) ; 5Aunque falla en un caso: si la lista est´a inicialmente vac´ıa. Estudiaremos este problema y su soluci´on m´as adelante. 264 Introducci´on a la Programaci´on con C
  • 271. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica El bucle se limita a ((desplazar)) el puntero aux hasta que apunte al ´ultimo elemento de la lista. Esta expresi´on del bucle que busca el elemento final es m´as propia de la programaci´on C, m´as idiom´atica. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 249 Hemos dise˜nado un m´etodo (que mejoraremos en el siguiente apartado) que permite insertar elementos por el final de una lista y hemos necesitado un bucle. ¿Har´a falta un bucle para insertar un elemento por delante en una lista cualquiera? ¿C´omo har´ıas para convertir la ´ultima lista en esta otra?: lista 1 info sig 3 info sig 8 info sig 2 info sig . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.5.4. Borrado de la cabeza Vamos a aprender ahora a borrar elementos de una lista. Empezaremos por ver c´omo eliminar el primer elemento de una lista. Nuestro objetivo es, partiendo de esta lista: lista 3 info sig 8 info sig 2 info sig llegar a esta otra: lista 8 info sig 2 info sig Como lo que deseamos es que lista pase a apuntar al segundo elemento de la lista, podr´ıamos dise˜nar una aproximaci´on directa modificando el valor de lista: 1 int main(void) 2 { 3 struct Nodo * lista = NULL, * aux, * nuevo; 4 5 ... 6 lista = lista->sig; // ! Mal! Se pierde la referencia a la cabeza original de la lista. 7 8 return 0; 9 } El efecto obtenido por esa acci´on es ´este: lista 3 info sig 8 info sig 2 info sig Efectivamente, hemos conseguido que la lista apuntada por lista sea lo que pretend´ıamos, pero hemos perdido la referencia a un nodo (el que hasta ahora era el primero) y ya no podemos liberarlo. Hemos provocado una fuga de memoria. Para liberar un bloque de memoria hemos de llamar a free con el puntero que apunta a la direcci´on en la que empieza el bloque. Nuestro bloque est´a apuntado por lista, as´ı que podr´ıamos pensar que la soluci´on es trivial y que bastar´ıa con llamar a free antes de modificar lista: 1 int main(void) 2 { 3 struct Nodo * lista = NULL, * aux, * nuevo; 4 5 ... 6 free(lista); 7 lista = lista->sig ; // ! Mal! lista no apunta a una zona de memoria v´alida. 8 9 return 0; 10 } Introducci´on a la Programaci´on con C 265
  • 272. 4.5 Introducci´on a la gesti´on de registros enlazados Fugas de memoria, colapsos y recogida de basura Muchos programas funcionan correctamente. . . durante un rato. Cuando llevan un tiempo ejecut´andose, sin embargo, el ordenador empieza a ralentizarse sin explicaci´on aparente y la memoria del ordenador se va agotando. Una de las razones para que esto ocurra son las fugas de memoria. Si el programa pide bloques de memoria con malloc y no los libera con free, ir´a consumiendo m´as y m´as memoria irremediablemente. Llegar´a un momento en que no quede apenas memoria libre y la que quede, estar´a muy fragmentada, as´ı que las peticiones a malloc costar´an m´as y m´as tiempo en ser satisfechas. . . ¡si es que pueden ser satisfechas! La saturaci´on de la memoria provocada por la fuga acabar´a colapsando al ordenador y, en algunos sistemas operativos, obligando a reiniciar la m´aquina. El principal problema con las fugas de memoria es lo dif´ıciles de detectar que resultan. Si pruebas el programa en un ordenador con mucha memoria, puede que no llegues a apreciar efecto negativo alguno al efectuar pruebas. Dar por bueno un programa err´oneo es, naturalmente, peor que saber que el programa a´un no es correcto. Los lenguajes de programaci´on modernos suelen evitar las fugas de memoria propor- cionando recogida de basura (del ingl´es garbage collection) autom´atica. Los sistemas de recogida de basura detectan las p´erdidas de referencia (origen de las fugas de memoria) y llaman autom´aticamente a free por nosotros. El programador s´olo escribe llamadas a malloc (o la funci´on/mecanismo equivalente en su lenguaje) y el sistema se encarga de marcar como disponibles los bloques de memoria no referenciados. Lenguajes como Python, Perl, Java, Ruby, Tcl y un largo etc´etera tiene recogida de basura autom´atica, aunque todos deben la idea a Lisp un lenguaje dise˜nado en los a˜nos 50 (¡¡¡!!!) que ya incorporaba esta ((avanzada)) caracter´ıstica. Pero, claro, no iba a resultar tan sencillo. ¡La l´ınea 7, que dice ((lista = lista->sig)), no puede ejecutarse! Tan pronto hemos ejecutado la l´ınea 6, tenemos otra fuga de memoria: lista 8 info sig 2 info sig O sea, hemos liberado correctamente el primer nodo, pero ahora hemos perdido la referencia al resto de nodos y el valor de lista->sig est´a indefinido. ¿C´omo podemos arreglar esto? Si no liberamos memoria, hay una fuga, y si la liberamos perdemos la referencia al resto de la lista. La soluci´on es sencilla: guardamos una referencia al resto de la lista con un puntero auxiliar cuando a´un estamos a tiempo. 1 int main(void) 2 { 3 struct Nodo * lista = NULL, * aux, * nuevo; 4 5 ... 6 aux = lista->sig ; 7 free(lista); 8 lista = aux; 9 10 return 0; 11 } Ahora s´ı. Veamos paso a paso qu´e hacen las ´ultimas tres l´ıneas del programa. La asignaci´on aux = lista->sig introduce una referencia al segundo nodo: aux lista 3 info sig 8 info sig 2 info sig Al ejecutar free(lista), pasamos a esta otra situaci´on: aux lista 8 info sig 2 info sig 266 Introducci´on a la Programaci´on con C
  • 273. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica No hay problema. Seguimos sabiendo d´onde est´a el resto de la lista: ((cuelga)) de aux. As´ı pues, podemos llegar al resultado deseado con la asignaci´on lista = aux: aux lista 8 info sig 2 info sig ¿Vas viendo ya el tipo de problemas al que nos enfrentamos con la gesti´on de listas? Los siguientes apartados te presentan funciones capaces de inicializar listas, de insertar, borrar y encontrar elementos, de mantener listas ordenadas, etc. Cada apartado te presentar´a una variante de las listas enlazadas con diferentes prestaciones que permiten elegir soluciones de compromiso entre velocidad de ciertas operaciones, consumo de memoria y complicaci´on de la implementaci´on. 4.6. Listas con enlace simple Vamos a desarrollar un m´odulo que permita manejar listas de enteros. En el fichero de cabecera declararemos los tipos de datos b´asicos: lista.h struct Nodo { int info; struct Nodo * sig; }; Como ya dijimos, este tipo de nodo s´olo alberga un n´umero entero. Si necesit´asemos una lista de float deber´ıamos cambiar el tipo del valor del campo info. Y si quisi´esemos una lista de ((personas)), podr´ıamos a˜nadir varios campos a struct Nodo (uno para el nombre, otro para la edad, etc.) o declarar info como de un tipo struct Persona definido previamente por nosotros. Una lista es un puntero a un struct Nodo, pero cuesta poco definir un nuevo tipo para referirnos con mayor brevedad al tipo ((lista)): lista.h ... typedef struct Nodo * TipoLista; Ahora, podemos declarar una lista como struct Nodo * o como TipoLista, indistintamente. Por claridad, nos referiremos al tipo de una lista con TipoLista y al de un puntero a un nodo cualquiera con struct Nodo *, pero no olvides que ambos tipos son equivalentes. Definici´on de struct con typedef Hay quienes, para evitar la escritura repetida de la palabra struct, recurren a la inmediata creaci´on de un nuevo tipo tan pronto se define el struct. Este c´odigo, por ejemplo, hace eso: 1 typedef struct Nodo { 2 int info; 3 struct Nodo * sig; 4 } TipoNodo; Como struct Nodo y TipoNodo son sin´onimos, pronto se intenta definir la estructura as´ı: 1 typedef struct Nodo { 2 int info; 3 TipoNodo * sig; // ! Mal! 4 } TipoNodo; Pero el compilador emite un aviso de error. La raz´on es simple: la primera aparici´on de la palabra TipoNodo tiene lugar antes de su propia definici´on. Introducci´on a la Programaci´on con C 267
  • 274. 4.6 Listas con enlace simple 4.6.1. Creaci´on de lista vac´ıa Nuestra primera funci´on crear´a una lista vac´ıa. El prototipo de la funci´on, que declaramos en la cabecera lista.h, es ´este: lista.h ... extern TipoLista lista_vacia(void); y la implementaci´on, que proporcionamos en una unidad de compilaci´on lista.c, resulta trivial: lista.c 1 #include <stdlib.h> 2 #include "lista.h" 3 4 TipoLista lista_vacia(void) 5 { 6 return NULL; 7 } La forma de uso es muy sencilla: 1 #include <stdlib.h> 2 #include "lista.h" 3 4 int main(void) 5 { 6 TipoLista lista; 7 8 lista = lista_vacia(); 9 10 return 0; 11 } Ciertamente podr´ıamos haber hecho lista = NULL, sin m´as, pero queda m´as elegante propor- cionar funciones para cada una de las operaciones b´asicas que ofrece una lista, y crear una lista vac´ıa es una operaci´on b´asica. 4.6.2. ¿Lista vac´ıa? Nos vendr´a bien disponer de una funci´on que devuelva cierto o falso en funci´on de si la lista est´a vac´ıa o no. El prototipo de la funci´on es: lista.h ... extern int es_lista_vacia(TipoLista lista); y su implementaci´on, muy sencilla: lista.c 1 int es_lista_vacia(TipoLista lista) 2 { 3 return lista == NULL; 4 } 4.6.3. Inserci´on por cabeza Ahora vamos a crear una funci´on que inserta un elemento en una lista por la cabeza, es decir, haciendo que el nuevo nodo sea el primero de la lista. Antes, veamos cu´al es el prototipo de la funci´on: lista.h ... extern TipoLista inserta_por_cabeza(TipoLista lista, int valor); 268 Introducci´on a la Programaci´on con C
  • 275. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica La forma de uso de la funci´on ser´a ´esta: miprograma.c 1 #include "lista.h" 2 3 int main(void) 4 { 5 TipoLista lista; 6 7 lista = lista_vacia(); 8 lista = inserta_por_cabeza(lista, 2); 9 lista = inserta_por_cabeza(lista, 8); 10 lista = inserta_por_cabeza(lista, 3); 11 ... 12 return 0; 13 } o, equivalentemente, esta otra: miprograma.c 1 #include "lista.h" 2 3 int main(void) 4 { 5 TipoLista lista; 6 7 lista = inserta_por_cabeza(inserta_por_cabeza(inserta_por_cabeza(lista_vacia(),2),8),3); 8 ... 9 return 0; 10 } Vamos con la implementaci´on de la funci´on. La funci´on debe empezar pidiendo un nuevo nodo para el n´umero que queremos insertar. lista.c 1 TipoLista inserta_por_cabeza(TipoLista lista, int valor) 2 { 3 struct Nodo * nuevo = malloc(sizeof(struct Nodo)); 4 5 nuevo->info = valor; 6 ... 7 } Ahora hemos de pensar un poco. Si lista va a tener como primer elemento a nuevo, ¿podemos enlazar directamente lista con nuevo? lista.c 1 TipoLista inserta_por_cabeza(TipoLista lista, int valor)@mal 2 { 3 struct Nodo * nuevo = malloc(sizeof(struct Nodo)); 4 5 nuevo->info = valor; 6 lista = nuevo ; 7 ... 8 } La respuesta es no. A´un no podemos. Si lo hacemos, no hay forma de enlazar nuevo->sig con lo que era la lista anteriormente. Hemos perdido la referencia a la lista original. Ve´amoslo con un ejemplo. Imagina una lista como ´esta: lista 8 info sig 2 info sig La ejecuci´on de la funci´on (incompleta) con valor igual a 3 nos lleva a esta otra situaci´on: Introducci´on a la Programaci´on con C 269
  • 276. 4.6 Listas con enlace simple nuevo 3 info sig lista 8 info sig 2 info sig Hemos perdido la referencia a la ((vieja)) lista. Una soluci´on sencilla consiste en, antes de modi- ficar lista, asignar a nuevo->sig el valor de lista: 1 TipoLista inserta_por_cabeza(TipoLista lista, int valor) 2 { 3 struct Nodo * nuevo = malloc(sizeof(struct Nodo)); 4 5 nuevo->info = valor; 6 nuevo->sig = lista ; 7 lista = nuevo; 8 return lista; 9 } Tras ejecutarse la l´ınea 3, tenemos: nuevo info sig lista 8 info sig 2 info sig Las l´ıneas 5 y 6 modifican los campos del nodo apuntado por nuevo: nuevo 3 info sig lista 8 info sig 2 info sig Finalmente, la l´ınea 7 hace que lista apunte a donde nuevo apunta. El resultado final es ´este: nuevo 3 info sig lista 8 info sig 2 info sig S´olo resta redisponer gr´aficamente la lista para que no quepa duda de la correcci´on de la soluci´on: nuevo lista 3 info sig 8 info sig 2 info sig Hemos visto, pues, que el m´etodo es correcto cuando la lista no est´a vac´ıa. ¿Lo ser´a tambi´en si suministramos una lista vac´ıa? La lista vac´ıa es un caso especial para el que siempre deberemos considerar la validez de nuestros m´etodos. Hagamos una comprobaci´on gr´afica. Si partimos de esta lista: lista y ejecutamos la funci´on (con valor igual a 10, por ejemplo), pasaremos moment´aneamente por esta situaci´on: nuevo 10 info sig lista 270 Introducci´on a la Programaci´on con C
  • 277. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica y llegaremos, al final, a esta otra: nuevo lista 10 info sig Ha funcionado correctamente. No tendremos tanta suerte con todas las funciones que vamos a dise˜nar. 4.6.4. Longitud de una lista Nos interesa conocer ahora la longitud de una lista. La funci´on que dise˜naremos recibe una lista y devuelve un entero: lista.h ... extern int longitud_lista(TipoLista lista); La implementaci´on se basa en recorrer toda la lista con un bucle que desplace un puntero hasta llegar a NULL. Con cada salto de nodo a nodo, incrementaremos un contador cuyo valor final ser´a devuelto por la funci´on: lista.c 1 int longitud_lista(TipoLista lista) 2 { 3 struct Nodo * aux; 4 int contador = 0; 5 6 for (aux = lista; aux != NULL; aux = aux->sig) 7 contador++; 8 return contador; 9 } Hagamos una peque˜na traza. Si recibimos esta lista: lista 3 info sig 8 info sig 2 info sig la variable contador empieza valiendo 0 y el bucle inicializa aux haciendo que apunte al primer elemento: aux lista 3 info sig 8 info sig 2 info sig En la primera iteraci´on, contador se incrementa en una unidad y aux pasa a apuntar al segundo nodo: aux lista 3 info sig 8 info sig 2 info sig Acto seguido, en la segunda iteraci´on, contador pasa a valer 2 y aux pasa a apuntar al tercer nodo: aux lista 3 info sig 8 info sig 2 info sig Finalmente, contador vale 3 y aux pasa a apuntar a NULL: Introducci´on a la Programaci´on con C 271
  • 278. 4.6 Listas con enlace simple aux lista 3 info sig 8 info sig 2 info sig Ah´ı acaba el bucle. El valor devuelto por la funci´on es 3, el n´umero de nodos de la lista. Observa que longitud_lista tarda m´as cuanto mayor es la lista. Una lista con n nodos obliga a efectuar n iteraciones del bucle for. Algo similar (aunque sin manejar listas enlazadas) nos ocurr´ıa con strlen, la funci´on que calcula la longitud de una cadena. La forma de usar esta funci´on desde el programa principal es sencilla: miprograma.c 1 #include <stdio.h> 2 #include "lista.h" 3 4 int main(void) 5 { 6 TipoLista lista; 7 ... 8 printf ("Longitud: %dn", longitud_lista(lista)); 9 10 return 0; 11 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 250 ¿Funcionar´a correctamente longitud_lista cuando le pasamos una lista vac´ıa? · 251 Dise˜na una funci´on que reciba una lista de enteros con enlace simple y devuelva el valor de su elemento m´aximo. Si la lista est´a vac´ıa, se devolver´a el valor 0. · 252 Dise˜na una funci´on que reciba una lista de enteros con enlace simple y devuelva su media. Si la lista est´a vac´ıa, se devolver´a el valor 0. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.6.5. Impresi´on en pantalla Ahora que sabemos recorrer una lista no resulta en absoluto dif´ıcil dise˜nar un procedimiento que muestre el contenido de una lista en pantalla. El prototipo es ´este: lista.h ... extern void muestra_lista(TipoLista lista); y una posible implementaci´on, ´esta: lista.c 1 void muestra_lista(TipoLista lista) 2 { 3 struct Nodo * aux; 4 5 for (aux = lista; aux != NULL; aux = aux->sig) 6 printf ("%dn", aux->info); 7 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 253 Dise˜na un procedimiento que muestre el contenido de una lista al estilo Python. Por ejemplo, la lista de la ´ultima figura se mostrar´a como [3, 8, 2]. F´ıjate en que la coma s´olo aparece separando a los diferentes valores, no despu´es de todos los n´umeros. · 254 Dise˜na un procedimiento que muestre el contenido de una lista como se indica en el siguiente ejemplo. La lista formada por los valores 3, 8 y 2 se representar´a as´ı: ->[3]->[8]->[2]->| (La barra vertical representa a NULL.) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272 Introducci´on a la Programaci´on con C
  • 279. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica 4.6.6. Inserci´on por cola Dise˜nemos ahora una funci´on que inserte un nodo al final de una lista. Su prototipo ser´a: lista.h ... extern TipoLista inserta_por_cola(TipoLista lista, int valor); Nuestra funci´on se dividir´a en dos etapas: una primera que localice al ´ultimo elemento de la lista, y otra que cree el nuevo nodo y lo una a la lista. Aqu´ı tienes la primera etapa: E lista.c E 1 TipoLista inserta_por_cola(TipoLista lista, int valor) 2 { 3 struct Nodo * aux; 4 5 for (aux = lista; aux->sig != NULL; aux = aux->sig) ; 6 ... 7 } Analicemos paso a paso el bucle con un ejemplo. Imagina que la lista que nos suministran en lista ya tiene tres nodos: lista 3 info sig 8 info sig 2 info sig La primera iteraci´on del bucle hace que aux apunte al primer elemento de la lista: aux lista 3 info sig 8 info sig 2 info sig Habr´a una nueva iteraci´on si aux->sig es distinto de NULL, es decir, si el nodo apuntado por aux no es el ´ultimo de la lista. Es nuestro caso, as´ı que iteramos haciendo aux = aux->sig, o sea, pasamos a esta nueva situaci´on: aux lista 3 info sig 8 info sig 2 info sig ¿Sigue siendo cierto que aux->sig es distinto de NULL? S´ı. Avanzamos aux un nodo m´as a la derecha: aux lista 3 info sig 8 info sig 2 info sig ¿Y ahora? ¿Es cierto que aux->sig es distinto de NULL? No, es igual a NULL. Ya hemos llegado al ´ultimo nodo de la lista. F´ıjate en que hemos parado un paso antes que cuando cont´abamos el n´umero de nodos de una lista; entonces la condici´on de iteraci´on del bucle era otra: ((aux != NULL)). Podemos proceder con la segunda fase de la inserci´on: pedir un nuevo nodo y enlazarlo desde el actual ´ultimo nodo. Nos vendr´a bien un nuevo puntero auxiliar: E lista.c E 1 TipoLista inserta_por_cola(TipoLista lista, int valor) 2 { 3 struct Nodo * aux, * nuevo ; 4 5 for (aux = lista; aux->sig != NULL; aux = aux->sig) ; Introducci´on a la Programaci´on con C 273
  • 280. 4.6 Listas con enlace simple 6 nuevo = malloc(sizeof(struct Nodo)); 7 nuevo->info = valor; 8 nuevo->sig = NULL; 9 aux->sig = nuevo; 10 return lista; 11 } El efecto de la ejecuci´on de las nuevas l´ıneas, suponiendo que el valor es 10, es ´este: nuevo 10 info sig aux lista 3 info sig 8 info sig 2 info sig Est´a claro que ha funcionado correctamente, ¿no? Tal vez resulte de ayuda ver la misma es- tructura reordenada as´ı: lista 3 info sig 8 info sig 2 info sig 10 info sig aux nuevo Bien, entonces, ¿por qu´e hemos marcado la funci´on como incorrecta? Veamos qu´e ocurre si la lista que nos proporcionan est´a vac´ıa. Si la lista est´a vac´ıa, lista vale NULL. En la primera iteraci´on del bucle for asignaremos a aux el valor de lista, es decir, NULL. Para ver si pasamos a efectuar la primera iteraci´on, hemos de comprobar antes si aux->sig es distinto de NULL. ¡Pero es un error preguntar por el valor de aux->sig cuando aux es NULL! Un puntero a NULL no apunta a nodo alguno, as´ı que no podemos preguntar por el valor del campo sig de un nodo que no existe. ¡Ojo con este tipo de errores!: los accesos a memoria que no nos ((pertenece)) no son detectables por el compilador. Se manifiestan en tiempo de ejecuci´on y, normalmente, con consecuencias desastrosas6 , especialmente al efectuar escrituras de informaci´on. ¿C´omo podemos corregir la funci´on? Tratando a la lista vac´ıa como un caso especial: lista.c 1 TipoLista inserta_por_cola(TipoLista lista, int valor) 2 { 3 struct Nodo * aux, * nuevo; 4 5 if (lista == NULL) { 6 lista = malloc(sizeof(struct Nodo)); 7 lista->info = valor; 8 lista->sig = NULL; 9 } 10 else { 11 for (aux = lista; aux->sig != NULL; aux = aux->sig) ; 12 nuevo = malloc(sizeof(struct Nodo)); 13 nuevo->info = valor; 14 nuevo->sig = NULL; 15 aux->sig = nuevo; 16 } 17 return lista; 18 } Como puedes ver, el tratamiento de la lista vac´ıa es muy sencillo, pero especial. Ya te lo advertimos antes: comprueba siempre si tu funci´on se comporta adecuadamente en situaciones extremas. La lista vac´ıa es un caso para el que siempre deber´ıas comprobar la validez de tu aproximaci´on. La funci´on puede retocarse factorizando acciones comunes a los dos bloques del if-else: 6En Linux, por ejemplo, obtendr´as un error (t´ıpicamente ((Segmentation fault))) y se abortar´a inmediatamente la ejecuci´on del programa. En Microsoft Windows es frecuente que el ordenador ((se cuelgue)). 274 Introducci´on a la Programaci´on con C
  • 281. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica lista.c 1 TipoLista inserta_por_cola(TipoLista lista, int valor) 2 { 3 struct Nodo * aux, * nuevo; 4 5 nuevo = malloc(sizeof(struct Nodo)); 6 nuevo->info = valor; 7 nuevo->sig = NULL; 8 if (lista == NULL) 9 lista = nuevo; 10 else { 11 for (aux = lista; aux->sig != NULL; aux = aux->sig) ; 12 aux->sig = nuevo; 13 } 14 return lista; 15 } Mejor as´ı. 4.6.7. Borrado de la cabeza La funci´on que vamos a dise˜nar ahora recibe una lista y devuelve esa misma lista sin el nodo que ocupaba inicialmente la posici´on de cabeza. El prototipo ser´a ´este: lista.h ... extern TipoLista borra_cabeza(TipoLista lista); Implement´emosla. No podemos hacer simplemente lista = lista->sig. Ciertamente, ello con- seguir´ıa que, en principio, los nodos que ((cuelgan)) de lista formaran una lista correcta, pero estar´ıamos provocando una fuga de memoria al no liberar con free el nodo de la cabeza (ya lo vimos cuando introdujimos las listas enlazadas): lista 3 info sig 8 info sig 2 info sig Tampoco podemos empezar haciendo free(lista) para liberar el primer nodo, pues entonces perder´ıamos la referencia al resto de nodos. La memoria quedar´ıa as´ı: lista 3 info sig 8 info sig 2 info sig ¿Qui´en apuntar´ıa entonces al primer nodo de la lista? La soluci´on requiere utilizar un puntero auxiliar: E lista.c E 1 TipoLista borra_cabeza(TipoLista lista) 2 { 3 struct Nodo * aux; 4 5 aux = lista->sig; 6 free(lista); 7 lista = aux; 8 return lista; 9 } Ahora s´ı, ¿no? No. Falla en el caso de que lista valga NULL, es decir, cuando nos pasan una lista vac´ıa. La asignaci´on aux = lista->sig es err´onea si lista es NULL. Pero la soluci´on es muy sencilla en este caso: si nos piden borrar el nodo de cabeza de una lista vac´ıa, ¿qu´e hemos de hacer? ¡Absolutamente nada!: lista.c 1 TipoLista borra_cabeza(TipoLista lista) 2 { Introducci´on a la Programaci´on con C 275
  • 282. 4.6 Listas con enlace simple 3 struct Nodo * aux; 4 5 if (lista != NULL) { 6 aux = lista->sig; 7 free(lista); 8 lista = aux; 9 } 10 return lista; 11 } Tenlo siempre presente: si usas la expresi´on aux->sig para cualquier puntero aux, has de estar completamente seguro de que aux no es NULL. 4.6.8. Borrado de la cola Vamos a dise˜nar ahora una funci´on que elimine el ´ultimo elemento de una lista. He aqu´ı su prototipo: lista.h ... extern TipoLista borra_cola(TipoLista lista); Nuevamente, dividiremos el trabajo en dos fases: 1. localizar el ´ultimo nodo de la lista para liberar la memoria que ocupa, 2. y hacer que el hasta ahora pen´ultimo nodo tenga como valor de su campo sig a NULL. La primera fase consistir´a b´asicamente en esto: E lista.c E 1 TipoLista borra_cola(TipoLista lista) 2 { 3 struct Nodo * aux; 4 5 for (aux = lista; aux->sig != NULL; aux = aux->sig) ; 6 ... 7 } ¡Alto! Este mismo bucle ya nos di´o problemas cuando tratamos de insertar por la cola: no funciona correctamente con listas vac´ıas. De todos modos, el problema tiene f´acil soluci´on: no tiene sentido borrar nada de una lista vac´ıa. E lista.c E 1 TipoLista borra_cola(TipoLista lista) 2 { 3 struct Nodo * aux; 4 5 if (lista != NULL) { 6 for (aux = lista; aux->sig != NULL; aux = aux->sig) ; 7 ... 8 } 9 return lista; 10 } Ahora el bucle solo se ejecuta con listas no vac´ıas. Si partimos de esta lista: aux lista 3 info sig 8 info sig 2 info sig el bucle hace que aux acabe apuntando al ´ultimo nodo: 276 Introducci´on a la Programaci´on con C
  • 283. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica aux lista 3 info sig 8 info sig 2 info sig . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 255 ¿Seguro que el bucle de borra_cola funciona correctamente siempre? Piensa si hace lo correcto cuando se le pasa una lista formada por un solo elemento. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Si hemos localizado ya el ´ultimo nodo de la lista, hemos de liberar su memoria: E lista.c E 1 TipoLista borra_cola(TipoLista lista) 2 { 3 struct Nodo * aux; 4 5 if (lista != NULL) { 6 for (aux = lista; aux->sig != NULL; aux = aux->sig) ; 7 free(aux); 8 ... 9 } 10 } Llegamos as´ı a esta situaci´on: aux lista 3 info sig 8 info sig F´ıjate: s´olo nos falta conseguir que el nuevo ´ultimo nodo (el de valor igual a 8) tenga como valor del campo sig a NULL. Problema: ¿y c´omo sabemos cu´al es el ´ultimo nodo? No se puede saber. Ni siquiera utilizando un nuevo bucle de b´usqueda del ´ultimo nodo, ya que dicho bucle se basaba en que el ´ultimo nodo es reconocible porque tiene a NULL como valor de sig, y ahora el ´ultimo no apunta con sig a NULL. El ((truco)) consiste en usar otro puntero auxiliar y modificar el bucle de b´usqueda del ´ultimo para haga que el nuevo puntero auxiliar vaya siempre ((un paso por detr´as)) de aux. Observa: E lista.c E 1 TipoLista borra_cola(TipoLista lista) 2 { 3 struct Nodo * aux, * atras ; 4 5 if (lista != NULL) { 6 for (atras = NULL, aux = lista; aux->sig != NULL; atras = aux, aux = aux->sig) ; 7 free(aux); 8 ... 9 } 10 } F´ıjate en el nuevo aspecto del bucle for. Utilizamos una construcci´on sint´actica que a´un no conoces, as´ı que nos detendremos brevemente para explicarla. Los bucles for permiten trabajar con m´as de una inicializaci´on y con m´as de una acci´on de paso a la siguiente iteraci´on. Este bucle, por ejemplo, trabaja con dos variables enteras, una que toma valores crecientes y otra que toma valores decrecientes: 1 for (i=0, j=10; i<3; i++, j--) 2 printf ("%d %dn", i, j); ¡Ojo! Es un ´unico bucle, no son dos bucles anidados. ¡No te confundas! Las diferentes inicia- lizaciones y pasos de iteraci´on se separan con comas. Al ejecutarlo, por pantalla aparecer´a esto: 0 10 1 9 2 8 Introducci´on a la Programaci´on con C 277
  • 284. 4.6 Listas con enlace simple Sigamos con el problema que nos ocupa. Veamos, paso a paso, qu´e hace ahora el bucle. En la primera iteraci´on tenemos: atras aux lista 3 info sig 8 info sig 2 info sig Y en la segunda iteraci´on: atras aux lista 3 info sig 8 info sig 2 info sig Y en la tercera: atras aux lista 3 info sig 8 info sig 2 info sig ¿Ves? No importa cu´an larga sea la lista; el puntero atras siempre va un paso por detr´as del puntero aux. En nuestro ejemplo ya hemos llegado al final de la lista, as´ı que ahora podemos liberar el nodo apuntado por aux: atras aux lista 3 info sig 8 info sig Ahora podemos continuar: ya hemos borrado el ´ultimo nodo, pero esta vez s´ı que sabemos cu´al es el nuevo ´ultimo nodo. E lista.c E 1 TipoLista borra_cola(TipoLista lista) 2 { 3 struct Nodo * aux, * atras ; 4 5 if (lista != NULL) { 6 for (atras = NULL, aux = lista; aux->sig != NULL; atras = aux, aux = aux->sig) ; 7 free(aux); 8 atras->sig = NULL; 9 ... 10 } 11 } Tras ejecutar la nueva sentencia, tenemos: atras aux lista 3 info sig 8 info sig A´un no hemos acabado. La funci´on borra_cola trabaja correctamente con la lista vac´ıa, pues no hace nada en ese caso (no hay nada que borrar), pero, ¿funciona correctamente cuando suministramos una lista con un ´unico elemento? Hagamos una traza. Tras ejecutar el bucle que busca a los elementos ´ultimo y pen´ultimo, los punteros atras y aux quedan as´ı: atras aux lista 3 info sig 278 Introducci´on a la Programaci´on con C
  • 285. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica Ahora se libera el nodo apuntado por aux: atras aux lista Y, finalmente, hacemos que atras->sig sea igual NULL. Pero, ¡eso es imposible! El puntero atras apunta a NULL, y hemos dicho ya que NULL no es un nodo y, por tanto, no tiene campo alguno. Tratemos este caso como un caso especial. En primer lugar, ¿c´omo podemos detectarlo? Viendo si atras vale NULL. ¿Y qu´e hemos de hacer entonces? Hemos de hacer que lista pase a valer NULL, sin m´as. lista.c 1 TipoLista borra_cola(TipoLista lista) 2 { 3 struct Nodo * aux, * atras ; 4 5 if (lista != NULL) { 6 for (atras = NULL, aux = lista; aux->sig != NULL; atras = aux, aux = aux->sig) ; 7 free(aux); 8 if (atras == NULL) 9 lista = NULL; 10 else 11 atras->sig = NULL; 12 } 13 return lista; 14 } Ya est´a. Si aplic´asemos este nuevo m´etodo, nuestro ejemplo concluir´ıa as´ı: atras aux lista Hemos aprendido una lecci´on: otro caso especial que conviene estudiar expl´ıcitamente es el de la lista compuesta por un solo elemento. Insistimos en que debes seguir una sencilla regla en el dise˜no de funciones con punteros: si accedes a un campo de un puntero ptr, por ejemplo, ptr->sig o ptr->info, preg´untate siempre si cabe alguna posibilidad de que ptr sea NULL; si es as´ı, tienes un problema que debes solucionar. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 256 ¿Funcionan correctamente las funciones que hemos definido antes (c´alculo de la lon- gitud, inserci´on por cabeza y por cola y borrado de cabeza) cuando se suministra una lista compuesta por un ´unico elemento? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.6.9. B´usqueda de un elemento Vamos a dise˜nar ahora una funci´on que no modifica la lista. Se trata de una funci´on que nos indica si un valor entero pertenece a la lista o no. El prototipo de la funci´on ser´a ´este: lista.h ... extern int pertenece(TipoLista lista, int valor); La funci´on devolver´a 1 si valor est´a en la lista y 0 en caso contrario. ¿Qu´e aproximaci´on seguiremos? Pues la misma que segu´ıamos con los vectores: recorrer cada uno de sus elementos y, si encontramos uno con el valor buscado, devolver inmediatamente el valor 1; si llegamos al final de la lista, ser´a que no lo hemos encontrado, as´ı que en tal caso devolveremos el valor 0. Introducci´on a la Programaci´on con C 279
  • 286. 4.6 Listas con enlace simple lista.c 1 int pertenece(TipoLista lista, int valor) 2 { 3 struct Nodo * aux; 4 5 for (aux=lista; aux != NULL; aux = aux->sig) 6 if (aux->info == valor) 7 return 1; 8 return 0; 9 } ´Esta ha sido f´acil, ¿no? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 257 ¿Funciona correctamente pertenece cuando se suministra NULL como valor de lista, es decir, cuando se suministra una lista vac´ıa? ¿Y cuando se suministra una lista con un ´unico elemento? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.6.10. Borrado del primer nodo con un valor determinado El problema que abordamos ahora es el dise˜no de una funci´on que recibe una lista y un valor y elimina el primer nodo de la lista cuyo campo info coincide con el valor. lista.h ... extern TipoLista borra_primera_ocurrencia(TipoLista lista, int valor); Nuestro primer problema consiste en detectar el valor en la lista. Si el valor no est´a en la lista, el problema se resuelve de forma trivial: se devuelve la lista intacta y ya est´a. E lista.c E 1 TipoLista borra_primera_ocurrencia(TipoLista lista, int valor) 2 { 3 struct Nodo * aux; 4 5 for (aux=lista; aux != NULL; aux = aux->sig) 6 if (aux->info == valor) { 7 ... 8 } 9 return lista; 10 11 } Veamos con un ejemplo en qu´e situaci´on estamos cuando llegamos a la l´ınea marcada con puntos suspensivos. En esta lista hemos buscado el valor 8, as´ı que podemos representar la memoria as´ı: aux lista 3 info sig 8 info sig 2 info sig Nuestro objetivo ahora es, por una parte, efectuar el siguiente ((empalme)) entre nodos: aux lista 3 info sig 8 info sig 2 info sig y, por otra, eliminar el nodo apuntado por aux: 280 Introducci´on a la Programaci´on con C
  • 287. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica aux lista 3 info sig 2 info sig Problema: ¿c´omo hacemos el ((empalme))? Necesitamos conocer cu´al es el nodo que precede al que apunta aux. Eso sabemos hacerlo con ayuda de un puntero auxiliar que vaya un paso por detr´as de aux: E lista.c E 1 TipoLista borra_primera_ocurrencia(TipoLista lista, int valor) 2 { 3 struct Nodo * aux, * atras ; 4 5 for (atras = NULL, aux=lista; aux != NULL; atras = aux , aux = aux->sig) 6 if (aux->info == valor) { 7 atras->sig = aux->sig; 8 ... 9 } 10 return lista; 11 } El puntero atras empieza apuntando a NULL y siempre va un paso por detr´as de aux. atras aux lista 3 info sig 8 info sig 2 info sig Es decir, cuando aux apunta a un nodo, atras apunta al anterior. La primera iteraci´on cambia el valor de los punteros y los deja en este estado: atras aux lista 3 info sig 8 info sig 2 info sig ¿Es correcta la funci´on? Hay una fuente de posibles problemas. Estamos asignando algo a atras->sig. ¿Cabe alguna posibilidad de que atras sea NULL? S´ı. El puntero atras es NULL cuando el elemento encontrado ocupa la primera posici´on. F´ıjate en este ejemplo en el que queremos borrar el elemento de valor 3: atras aux lista 3 info sig 8 info sig 2 info sig El ((empalme)) procedente en este caso es ´este: atras aux lista 3 info sig 8 info sig 2 info sig lista.c 1 TipoLista borra_primera_ocurrencia(TipoLista lista, int valor) 2 { 3 struct Nodo * aux, * atras ; 4 5 for (atras = NULL, aux=lista; aux != NULL; atras = aux, aux = aux->sig) 6 if (aux->info == valor) { 7 if (atras == NULL) Introducci´on a la Programaci´on con C 281
  • 288. 4.6 Listas con enlace simple 8 lista = aux->sig; 9 else 10 atras->sig = aux->sig; 11 ... 12 } 13 return lista; 14 } Ahora podemos borrar el elemento apuntado por aux con tranquilidad y devolver la lista mo- dificada: lista.c 1 TipoLista borra_primera_ocurrencia(TipoLista lista, int valor) 2 { 3 struct Nodo * aux, * atras ; 4 5 for (atras = NULL, aux=lista; aux != NULL; atras = aux, aux = aux->sig) 6 if (aux->info == valor) { 7 if (atras == NULL) 8 lista = aux->sig; 9 else 10 atras->sig = aux->sig; 11 free(aux); 12 return lista; 13 } 14 return lista; 15 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 258 ¿Funciona borra_primera_ocurrencia cuando ning´un nodo de la lista contiene el valor buscado? · 259 ¿Funciona correctamente en los siguientes casos? lista vac´ıa; lista con un s´olo elemento que no coincide en valor con el buscado; lista con un s´olo elemento que coincide en valor con el buscado. Si no es as´ı, corrige la funci´on. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.6.11. Borrado de todos los nodos con un valor dado Borrar todos los nodos con un valor dado y no s´olo el primero es bastante m´as complicado, aunque hay una idea que conduce a una soluci´on trivial: llamar tantas veces a la funci´on que hemos dise˜nado en el apartado anterior como elementos haya originalmente en la lista. Pero, como comprender´as, se trata de una aproximaci´on muy ineficiente: si la lista tiene n nodos, llamaremos n veces a una funci´on que, en el peor de los casos, recorre la lista completa, es decir, da n ((pasos)) para completarse. Es m´as eficiente borrar todos los elementos de una sola pasada, en tiempo directamente proporcional a n. Sup´on que recibimos esta lista: lista 3 info sig 8 info sig 2 info sig 8 info sig 1 info sig y nos piden eliminar todos los nodos cuyo campo info vale 8. Nuestro problema es localizar el primer 8 y borrarlo dejando los dos punteros auxiliares en un estado tal que podamos seguir iterando para encontrar y borrar el siguiente 8 en la lista (y as´ı con todos los que haya). Ya sabemos c´omo localizar el primer 8. Si usamos un bucle con dos punteros (aux y atras), llegamos a esta situaci´on: 282 Introducci´on a la Programaci´on con C
  • 289. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica atras aux lista 3 info sig 8 info sig 2 info sig 8 info sig 1 info sig Si eliminamos el nodo apuntado por aux, nos interesa que aux pase a apuntar al siguiente, pero que atras quede apuntando al mismo nodo al que apunta ahora (siempre ha de ir un paso por detr´as de aux): atras aux lista 3 info sig 2 info sig 8 info sig 1 info sig Bueno. No resultar´a tan sencillo. Deberemos tener en cuenta qu´e ocurre en una situaci´on especial: el borrado del primer elemento de una lista. Aqu´ı tienes una soluci´on: lista.c 1 TipoLista borra_valor(TipoLista lista, int valor) 2 { 3 struct Nodo * aux, * atras ; 4 5 atras = NULL; 6 aux = lista; 7 while (aux != NULL) { 8 if (aux->info == valor) { 9 if (atras == NULL) 10 lista = aux->sig; 11 else 12 atras->sig = aux->sig; 13 free(aux); 14 if (atras == NULL) 15 aux = lista; 16 else 17 aux = atras->sig; 18 } 19 else { 20 atras = aux; 21 aux = aux->sig; 22 } 23 } 24 return lista; 25 } Hemos optado por un bucle while en lugar de un bucle for porque necesitamos un mayor control de los punteros auxiliares (con el for, en cada iteraci´on avanzamos ambos punteros y no siempre queremos que avancen). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 260 ¿Funciona borra_valor con listas vac´ıas? ¿Y con listas de un s´olo elemento? ¿Y con una lista en la que todos los elementos coinciden en valor con el entero que buscamos? Si falla en alguno de estos casos, corrige la funci´on. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.6.12. Inserci´on en una posici´on dada Vamos a dise˜nar una funci´on que permite insertar un nodo en una posici´on dada de la lista. Asumiremos la siguiente numeraci´on de posiciones en una lista: lista 3 info sig 8 info sig 2 info sig 0 1 2 3 Introducci´on a la Programaci´on con C 283
  • 290. 4.6 Listas con enlace simple while y for Hemos dicho que el bucle for no resulta conveniente cuando queremos tener un gran control sobre los punteros auxiliares. No es cierto. El bucle for de C permite emular a cualquier bucle while. Aqu´ı tienes una versi´on de borra_valor (eliminaci´on de todos los nodos con un valor dado) que usa un bucle for: 1 TipoLista borra_valor(TipoLista lista, int valor) 2 { 3 struct Nodo * aux, * atras; 4 5 for (atras = NULL, aux = lista; aux != NULL; ) { 6 if (aux->info == valor) { 7 if (atras == NULL) 8 lista = aux->sig; 9 else 10 atras->sig = aux->sig; 11 free(aux); 12 if (atras == NULL) 13 aux = lista; 14 else 15 aux = atras->sig; 16 } 17 else { 18 atras = aux; 19 aux = aux->sig; 20 } 21 } 22 return lista; 23 } Observa que en el bucle for hemos dejado en blanco la zona que indica c´omo modificar los punteros aux y atras. Puede hacerse. De hecho, puedes dejar en blanco cualquiera de los componentes de un bucle for. Una alternativa a while (1), por ejemplo, es for (;;). O sea, insertar en la posici´on 0 es insertar una nueva cabeza; en la posici´on 1, un nuevo segundo nodo, etc. ¿Qu´e pasa si se quiere insertar un nodo en una posici´on mayor que la longitud de la lista? Lo insertaremos en ´ultima posici´on. El prototipo de la funci´on ser´a: lista.h ... extern TipoLista inserta_en_posicion(TipoLista lista, int pos, int valor); y aqu´ı tienes su implementaci´on: lista.c 1 TipoLista inserta_en_posicion(TipoLista lista, int pos, int valor) 2 { 3 struct Nodo * aux, * atras, * nuevo; 4 int i; 5 6 nuevo = malloc(sizeof(struct Nodo)); 7 nuevo->info = valor; 8 9 for (i=0, atras=NULL, aux=lista; i < pos && aux != NULL; i++, atras = aux, aux = aux->sig) ; 10 nuevo->sig = aux; 11 if (atras == NULL) 12 lista = nuevo; 13 else 14 atras->sig = nuevo; 15 return lista; 16 } 284 Introducci´on a la Programaci´on con C
  • 291. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 261 Modifica la funci´on para que, si nos pasan un n´umero de posici´on mayor que el n´umero de elementos de la lista, no se realice inserci´on alguna. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.6.13. Inserci´on ordenada Las listas que hemos manejado hasta el momento est´an desordenadas, es decir, sus nodos est´an dispuestos en un orden arbitrario. Es posible mantener listas ordenadas si las inserciones se realizan utilizando siempre una funci´on que respete el orden. La funci´on que vamos a desarrollar, por ejemplo, inserta un elemento en una lista ordenada de menor a mayor de modo que la lista resultante sea tambi´en una lista ordenada de menor a mayor. lista.c 1 TipoLista inserta_en_orden(TipoLista lista, int valor); 2 { 3 struct Nodo * aux, * atras, * nuevo; 4 5 nuevo = malloc(sizeof(struct Nodo)); 6 nuevo->info = valor; 7 8 for (atras = NULL, aux = lista; aux != NULL; atras = aux, aux = aux->sig) 9 if (valor <= aux->info) { 10 /* Aqu´ı insertamos el nodo entre atras y aux. */ 11 nuevo->sig = aux; 12 if (atras == NULL) 13 lista = nuevo; 14 else 15 atras->sig = nuevo; 16 /* Y como ya est´a insertado, acabamos. */ 17 return lista; 18 } 19 /* Si llegamos aqu´ı, es que nuevo va al final de la lista. */ 20 nuevo->sig = NULL; 21 if (atras == NULL) 22 lista = nuevo; 23 else 24 atras->sig = nuevo; 25 return lista; 26 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 262 Haz una traza de la inserci´on del valor 7 con inserta_en_orden en cada una de estas listas: a) lista 1 info sig 3 info sig 8 info sig b) lista 12 info sig 15 info sig 23 info sig c) lista 1 info sig 7 info sig 9 info sig Introducci´on a la Programaci´on con C 285
  • 292. 4.6 Listas con enlace simple d) lista e) lista 1 info sig f) lista 10 info sig · 263 Dise˜na una funci´on de inserci´on ordenada en lista que inserte un nuevo nodo si y s´olo si no hab´ıa ning´un otro con el mismo valor. · 264 Determinar la pertenencia de un valor a una lista ordenada no requiere que recorras siempre toda la lista. Dise˜na una funci´on que determine la pertenencia a una lista ordenada efectuando el menor n´umero posible de comparaciones y desplazamientos sobre la lista. · 265 Implementa una funci´on que ordene una lista cualquiera mediante el m´etodo de la burbuja. · 266 Dise˜na una funci´on que diga, devolviendo el valor 1 o el valor 0, si una lista est´a ordenada o desordenada. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.6.14. Concatenaci´on de dos listas La funci´on que dise˜naremos ahora recibe dos listas y devuelve una nueva lista que resulta de concatenar (una copia de) ambas. lista.c 1 TipoLista concatena_listas(TipoLista a, TipoLista b) 2 { 3 TipoLista c = NULL; 4 struct Nodo * aux, * nuevo, * anterior = NULL; 5 6 for (aux = a; aux != NULL; aux = aux->sig) { 7 nuevo = malloc( sizeof(struct Nodo) ); 8 nuevo->info = aux->info; 9 if (anterior != NULL) 10 anterior->sig = nuevo; 11 else 12 c = nuevo; 13 anterior = nuevo; 14 } 15 for (aux = b; aux != NULL; aux = aux->sig) { 16 nuevo = malloc( sizeof(struct Nodo) ); 17 nuevo->info = aux->info; 18 if (anterior != NULL) 19 anterior->sig = nuevo; 20 else 21 c = nuevo; 22 anterior = nuevo; 23 } 24 if (anterior != NULL) 25 anterior->sig = NULL; 26 return c; 27 } 286 Introducci´on a la Programaci´on con C
  • 293. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 267 Dise˜na una funci´on que a˜nada a una lista una copia de otra lista. · 268 Dise˜na una funci´on que devuelva una lista con los elementos de otra lista que sean mayores que un valor dado. · 269 Dise˜na una funci´on que devuelva una lista con los elementos comunes a otras dos listas. · 270 Dise˜na una funci´on que devuelva una lista que es una copia invertida de otra lista. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.6.15. Borrado de la lista completa Acabaremos este apartado con una rutina que recibe una lista y borra todos y cada uno de sus nodos. A estas alturas no deber´ıa resultarte muy dif´ıcil de entender: lista.c 1 TipoLista libera_lista(TipoLista lista) 2 { 3 struct Nodo *aux, *otroaux; 4 5 aux = lista; 6 while (aux != NULL) { 7 otroaux = aux->sig; 8 free(aux); 9 aux = otroaux; 10 } 11 return NULL; 12 } Alternativamente podr´ıamos definir la rutina de liberaci´on como un procedimiento: lista.c 1 void libera_lista(TipoLista * lista) 2 { 3 struct Nodo *aux, *otroaux; 4 5 aux = *lista; 6 while (aux != NULL) { 7 otroaux = aux->sig; 8 free(aux); 9 aux = otroaux; 10 } 11 *lista = NULL; 12 } De este modo nos aseguramos de que el puntero lista fija su valor a NULL. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 271 Dise˜na una funci´on que devuelva un ((corte)) de la lista. Se proporcionar´an como par´ametros dos enteros i y j y se devolver´a una lista con una copia de los nodos que ocu- pan las posiciones i a j − 1, ambas inclu´ıdas. · 272 Dise˜na una funci´on que elimine un ((corte)) de la lista. Se proporcionar´an como par´ametros dos enteros i y j y se eliminar´an los nodos que ocupan las posiciones i a j − 1, ambas inclu´ıdas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.6.16. Juntando las piezas Te ofrecemos, a modo de resumen, todas las funciones que hemos desarrollado a lo largo de la secci´on junto con un programa de prueba (faltan, naturalmente, las funciones cuyo desarrollo se propone como ejercicio). Introducci´on a la Programaci´on con C 287
  • 294. 4.6 Listas con enlace simple lista.h lista.h 1 struct Nodo { 2 int info; 3 struct Nodo * sig; 4 }; 5 6 typedef struct Nodo * TipoLista; 7 8 extern TipoLista lista_vacia(void); 9 extern int es_lista_vacia(TipoLista lista); 10 extern TipoLista inserta_por_cabeza(TipoLista lista, int valor); 11 extern TipoLista inserta_por_cola(TipoLista lista, int valor); 12 extern TipoLista borra_cabeza(TipoLista lista); 13 extern TipoLista borra_cola(TipoLista lista); 14 extern int longitud_lista(TipoLista lista); 15 extern void muestra_lista(TipoLista lista); 16 extern int pertenece(TipoLista lista, int valor); 17 extern TipoLista borra_primera_ocurrencia(TipoLista lista, int valor); 18 extern TipoLista borra_valor(TipoLista lista, int valor); 19 extern TipoLista inserta_en_posicion(TipoLista lista, int pos, int valor); 20 extern TipoLista inserta_en_orden(TipoLista lista, int valor); 21 extern TipoLista concatena_listas(TipoLista a, TipoLista b); 22 extern TipoLista libera_lista(TipoLista lista); lista.c lista.c 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include "lista.h" 4 5 TipoLista lista_vacia(void) 6 { 7 return NULL; 8 } 9 10 int es_lista_vacia(TipoLista lista) 11 { 12 return lista == NULL; 13 } 14 15 TipoLista inserta_por_cabeza(TipoLista lista, int valor) 16 { 17 struct Nodo * nuevo = malloc(sizeof(struct Nodo)); 18 19 nuevo->info = valor; 20 nuevo->sig = lista; 21 lista = nuevo; 22 return lista; 23 } 24 25 TipoLista inserta_por_cola(TipoLista lista, int valor) 26 { 27 struct Nodo * aux, * nuevo; 28 29 nuevo = malloc(sizeof(struct Nodo)); 30 nuevo->info = valor; 31 nuevo->sig = NULL; 32 if (lista == NULL) 33 lista = nuevo; 34 else { 35 for (aux = lista; aux->sig != NULL; aux = aux->sig) ; 36 aux->sig = nuevo; 37 } 288 Introducci´on a la Programaci´on con C
  • 295. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica 38 return lista; 39 } 40 41 TipoLista borra_cabeza(TipoLista lista) 42 { 43 struct Nodo * aux; 44 45 if (lista != NULL) { 46 aux = lista->sig; 47 free(lista); 48 lista = aux; 49 } 50 return lista; 51 } 52 53 TipoLista borra_cola(TipoLista lista) 54 { 55 struct Nodo * aux, * atras; 56 57 if (lista != NULL) { 58 for (atras = NULL, aux = lista; aux->sig != NULL; atras = aux, aux = aux->sig) ; 59 free(aux); 60 if (atras == NULL) 61 lista = NULL; 62 else 63 atras->sig = NULL; 64 } 65 return lista; 66 } 67 68 int longitud_lista(TipoLista lista) 69 { 70 struct Nodo * aux; 71 int contador = 0; 72 73 for (aux = lista; aux != NULL; aux = aux->sig) 74 contador++; 75 return contador; 76 } 77 78 void muestra_lista(TipoLista lista) 79 { // Como la soluci´on al ejercicio 254, no como lo vimos en el texto. 80 struct Nodo * aux; 81 82 printf ("->"); 83 for (aux = lista; aux != NULL; aux = aux->sig) 84 printf ("[%d]->", aux->info); 85 printf ("|n"); 86 } 87 88 int pertenece(TipoLista lista, int valor) 89 { 90 struct Nodo * aux; 91 92 for (aux=lista; aux != NULL; aux = aux->sig) 93 if (aux->info == valor) 94 return 1; 95 return 0; 96 } 97 98 TipoLista borra_primera_ocurrencia(TipoLista lista, int valor) 99 { 100 struct Nodo * aux, * atras; Introducci´on a la Programaci´on con C 289
  • 296. 4.6 Listas con enlace simple 101 102 for (atras = NULL, aux=lista; aux != NULL; atras = aux, aux = aux->sig) 103 if (aux->info == valor) { 104 if (atras == NULL) 105 lista = aux->sig; 106 else 107 atras->sig = aux->sig; 108 free(aux); 109 return lista; 110 } 111 return lista; 112 } 113 114 TipoLista borra_valor(TipoLista lista, int valor) 115 { 116 struct Nodo * aux, * atras; 117 118 atras = NULL; 119 aux = lista; 120 while (aux != NULL) { 121 if (aux->info == valor) { 122 if (atras == NULL) 123 lista = aux->sig; 124 else 125 atras->sig = aux->sig; 126 free(aux); 127 if (atras == NULL) 128 aux = lista; 129 else 130 aux = atras->sig; 131 } 132 else { 133 atras = aux; 134 aux = aux->sig; 135 } 136 } 137 return lista; 138 } 139 140 TipoLista inserta_en_posicion(TipoLista lista, int pos, int valor) 141 { 142 struct Nodo * aux, * atras, * nuevo; 143 int i; 144 145 nuevo = malloc(sizeof(struct Nodo)); 146 nuevo->info = valor; 147 148 for (i=0, atras=NULL, aux=lista; i < pos && aux != NULL; i++, atras = aux, aux = aux->sig) ; 149 nuevo->sig = aux; 150 if (atras == NULL) 151 lista = nuevo; 152 else 153 atras->sig = nuevo; 154 return lista; 155 } 156 157 TipoLista inserta_en_orden(TipoLista lista, int valor) 158 { 159 struct Nodo * aux, * atras, * nuevo; 160 161 nuevo = malloc(sizeof(struct Nodo)); 162 nuevo->info = valor; 163 290 Introducci´on a la Programaci´on con C
  • 297. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica 164 for (atras = NULL, aux = lista; aux != NULL; atras = aux, aux = aux->sig) 165 if (valor <= aux->info) { 166 /* Aqu´ı insertamos el nodo entre atras y aux. */ 167 nuevo->sig = aux; 168 if (atras == NULL) 169 lista = nuevo; 170 else 171 atras->sig = nuevo; 172 /* Y como ya est´a insertado, acabamos. */ 173 return lista; 174 } 175 /* Si llegamos aqu´ı, es que nuevo va al final de la lista. */ 176 nuevo->sig = NULL; 177 if (atras == NULL) 178 lista = nuevo; 179 else 180 atras->sig = nuevo; 181 return lista; 182 } 183 184 TipoLista concatena_listas(TipoLista a, TipoLista b) 185 { 186 TipoLista c = NULL; 187 struct Nodo * aux, * nuevo, * anterior = NULL; 188 189 for (aux = a; aux != NULL; aux = aux->sig) { 190 nuevo = malloc( sizeof(struct Nodo) ); 191 nuevo->info = aux->info; 192 if (anterior != NULL) 193 anterior->sig = nuevo; 194 else 195 c = nuevo; 196 anterior = nuevo; 197 } 198 for (aux = b; aux != NULL; aux = aux->sig) { 199 nuevo = malloc( sizeof(struct Nodo) ); 200 nuevo->info = aux->info; 201 if (anterior != NULL) 202 anterior->sig = nuevo; 203 else 204 c = nuevo; 205 anterior = nuevo; 206 } 207 if (anterior != NULL) 208 anterior->sig = NULL; 209 return c; 210 } 211 212 TipoLista libera_lista(TipoLista lista) 213 { 214 struct Nodo *aux, *otroaux; 215 216 aux = lista; 217 while (aux != NULL) { 218 otroaux = aux->sig; 219 free(aux); 220 aux = otroaux; 221 } 222 return NULL; 223 } prueba lista.c prueba lista.c 1 #include <stdio.h> Introducci´on a la Programaci´on con C 291
  • 298. 4.6 Listas con enlace simple 2 3 #include "lista.h" 4 5 int main(void) 6 { 7 TipoLista l, l2, l3; 8 9 printf ("Creaci´on de listan"); 10 l = lista_vacia(); 11 muestra_lista(l); 12 13 printf (" ? Es lista vac´ıa?: %dn", es_lista_vacia(l)); 14 15 printf ("Inserci´on por cabeza de 2, 8, 3n"); 16 l = inserta_por_cabeza(l, 2); 17 l = inserta_por_cabeza(l, 8); 18 l = inserta_por_cabeza(l, 3); 19 muestra_lista(l); 20 21 printf ("Longitud de la lista: %dn", longitud_lista(l)); 22 23 printf ("Inserci´on por cola de 1, 5, 10n"); 24 l = inserta_por_cola(l, 1); 25 l = inserta_por_cola(l, 5); 26 l = inserta_por_cola(l, 10); 27 muestra_lista(l); 28 29 printf ("Borrado de cabezan"); 30 l = borra_cabeza(l); 31 muestra_lista(l); 32 33 printf ("Borrado de colan"); 34 l = borra_cola(l); 35 muestra_lista(l); 36 37 printf (" ? Pertenece 5 a la lista: %dn", pertenece(l, 5)); 38 printf (" ? Pertenece 7 a la lista: %dn", pertenece(l, 7)); 39 40 printf ("Inserci´on por cola de 1n"); 41 l = inserta_por_cola(l, 1); 42 muestra_lista(l); 43 44 printf ("Borrado de primera ocurrencia de 1n"); 45 l = borra_primera_ocurrencia(l, 1); 46 muestra_lista(l); 47 48 printf ("Nuevo borrado de primera ocurrencia de 1n"); 49 l = borra_primera_ocurrencia(l, 1); 50 muestra_lista(l); 51 52 printf ("Nuevo borrado de primera ocurrencia de 1 (que no est´a)n"); 53 l = borra_primera_ocurrencia(l, 1); 54 muestra_lista(l); 55 56 printf ("Inserci´on por cola y por cabeza de 2n"); 57 l = inserta_por_cola(l, 2); 58 l = inserta_por_cabeza(l, 2); 59 muestra_lista(l); 60 61 printf ("Borrado de todas las ocurrencias de 2n"); 62 l = borra_valor(l, 2); 63 muestra_lista(l); 64 292 Introducci´on a la Programaci´on con C
  • 299. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica 65 printf ("Borrado de todas las ocurrencias de 8n"); 66 l = borra_valor(l, 8); 67 muestra_lista(l); 68 69 printf ("Inserci´on de 1 en posici´on 0n"); 70 l = inserta_en_posicion(l, 0, 1); 71 muestra_lista(l); 72 73 printf ("Inserci´on de 10 en posici´on 2n"); 74 l = inserta_en_posicion(l, 2, 10); 75 muestra_lista(l); 76 77 printf ("Inserci´on de 3 en posici´on 1n"); 78 l = inserta_en_posicion(l, 1, 3); 79 muestra_lista(l); 80 81 printf ("Inserci´on de 4, 0, 20 y 5 en ordenn"); 82 l = inserta_en_orden(l, 4); 83 l = inserta_en_orden(l, 0); 84 l = inserta_en_orden(l, 20); 85 l = inserta_en_orden(l, 5); 86 muestra_lista(l); 87 88 printf ("Creaci´on de una nueva lista con los elementos 30, 40, 50n"); 89 l2 = lista_vacia(); 90 l2 = inserta_por_cola(l2, 30); 91 l2 = inserta_por_cola(l2, 40); 92 l2 = inserta_por_cola(l2, 50); 93 muestra_lista(l2); 94 95 printf ("Concatenaci´on de las dos listas para formar una nuevan"); 96 l3 = concatena_listas(l, l2); 97 muestra_lista(l3); 98 99 printf ("Liberaci´on de las tres listasn"); 100 l = libera_lista(l); 101 l2 = libera_lista(l2); 102 l3 = libera_lista(l3); 103 muestra_lista(l); 104 muestra_lista(l2); 105 muestra_lista(l3); 106 107 return 0; 108 } Recuerda que debes compilar estos programas en al menos dos pasos: $ gcc lista.c -c $ gcc prueba_lista.c lista.o -o prueba_lista Este es el resultado en pantalla de la ejecuci´on de prueba lista: Creaci´on de lista ->| ? Es lista vac´ıa?: 1 Inserci´on por cabeza de 2, 8, 3 ->[3]->[8]->[2]->| Longitud de la lista: 3 Inserci´on por cola de 1, 5, 10 ->[3]->[8]->[2]->[1]->[5]->[10]->| Borrado de cabeza ->[8]->[2]->[1]->[5]->[10]->| Borrado de cola ->[8]->[2]->[1]->[5]->| Introducci´on a la Programaci´on con C 293
  • 300. 4.7 Listas simples con punteros a cabeza y cola ? Pertenece 5 a la lista: 1 ? Pertenece 7 a la lista: 0 Inserci´on por cola de 1 ->[8]->[2]->[1]->[5]->[1]->| Borrado de primera ocurrencia de 1 ->[8]->[2]->[5]->[1]->| Nuevo borrado de primera ocurrencia de 1 ->[8]->[2]->[5]->| Nuevo borrado de primera ocurrencia de 1 (que no est´a) ->[8]->[2]->[5]->| Inserci´on por cola y por cabeza de 2 ->[2]->[8]->[2]->[5]->[2]->| Borrado de todas las ocurrencias de 2 ->[8]->[5]->| Borrado de todas las ocurrencias de 8 ->[5]->| Inserci´on de 1 en posici´on 0 ->[1]->[5]->| Inserci´on de 10 en posici´on 2 ->[1]->[5]->[10]->| Inserci´on de 3 en posici´on 1 ->[1]->[3]->[5]->[10]->| Inserci´on de 4, 0, 20 y 5 en orden ->[0]->[1]->[3]->[4]->[5]->[5]->[10]->[20]->| Creaci´on de una nueva lista con los elementos 30, 40, 50 ->[30]->[40]->[50]->| Concatenaci´on de las dos listas para formar una nueva ->[0]->[1]->[3]->[4]->[5]->[5]->[10]->[20]->[30]->[40]->[50]->| Liberaci´on de las tres listas ->| ->| ->| 4.7. Listas simples con punteros a cabeza y cola Las listas que hemos estudiado hasta el momento son muy r´apidas para, por ejemplo, la inserci´on de elementos por la cabeza. Como la cabeza est´a permanentemente apuntada por un puntero, basta con pedir memoria para un nuevo nodo y hacer un par de ajustes con punteros: hacer que el nodo que sigue al nuevo nodo sea el que era apuntado por el puntero a cabeza, y hacer que el puntero a cabeza apunte ahora al nuevo nodo. No importa cu´an larga sea la lista: la inserci´on por cabeza es siempre igual de r´apida. Requiere una cantidad de tiempo constante. Pero la inserci´on por cola est´a seriamente penalizada en comparaci´on con la inserci´on por cabeza. Como no sabemos d´onde est´a el ´ultimo elemento, hemos de recorrer la lista completa cada vez que deseamos a˜nadir por la cola. Una forma de eliminar este problema consiste en mantener siempre dos punteros: uno al primer elemento de la lista y otro al ´ultimo. La nueva estructura de datos que representa una lista podr´ıa definirse as´ı: lista cabeza cola.h 1 struct Nodo { 2 int info; 3 struct Nodo * sig; 4 }; 5 6 struct Lista_cc { 7 struct Nodo * cabeza; 8 struct Nodo * cola; 9 }; Podemos representar gr´aficamente una lista con punteros a cabeza y cola as´ı: 294 Introducci´on a la Programaci´on con C
  • 301. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica lista cabeza cola 3 info sig 8 info sig 2 info sig Los punteros lista.cabeza y lista.cola forman un ´unico objeto del tipo lista_cc. Vamos a presentar ahora unas funciones que gestionan listas con punteros a cabeza y cola. Afortunadamente, todo lo aprendido con las listas del apartado anterior nos vale. Eso s´ı, algu- nas operaciones se simplificar´an notablemente (a˜nadir por la cola, por ejemplo), pero otras se complicar´an ligeramente (eliminar la cola, por ejemplo), ya que ahora hemos de encargarnos de mantener siempre un nuevo puntero (lista.cola) apuntando correctamente al ´ultimo elemento de la lista. 4.7.1. Creaci´on de lista vac´ıa La funci´on que crea una lista vac´ıa es, nuevamente, muy sencilla. El prototipo es ´este: lista cabeza cola.h 1 extern struct Lista_cc crea_lista_cc_vacia(void); y su implementaci´on: lista cabeza cola.c 1 struct Lista_cc crea_lista_cc_vacia(void) 2 { 3 struct Lista_cc lista; 4 lista.cabeza = lista.cola = NULL; 5 return lista; 6 } Una lista vac´ıa puede representarse as´ı: lista cabeza cola 4.7.2. Inserci´on de nodo en cabeza La inserci´on de un nodo en cabeza s´olo requiere, en principio, modificar el valor del campo cabeza, ¿no? Veamos, si tenemos una lista como ´esta: lista cabeza cola 3 info sig 8 info sig 2 info sig y deseamos insertar el valor 1 en cabeza, basta con modificar lista.cabeza y ajustar el campo sig del nuevo nodo para que apunte a la antigua cabeza. Como puedes ver, lista.cola sigue apuntando al mismo lugar al que apuntaba inicialmente: lista cabeza cola 1 info sig 3 info sig 8 info sig 2 info sig Ya est´a, ¿no? No. Hay un caso en el que tambi´en hemos de modificar lista.cola adem´as de lista.cabeza: cuando la lista est´a inicialmente vac´ıa. ¿Por qu´e? Porque el nuevo nodo de la lista ser´a cabeza y cola a la vez. F´ıjate, si partimos de esta lista: lista cabeza cola e insertamos el valor 1, hemos de construir esta otra: Introducci´on a la Programaci´on con C 295
  • 302. 4.7 Listas simples con punteros a cabeza y cola lista cabeza cola 1 info sig Si s´olo modific´asemos el valor de lista.cabeza, tendr´ıamos esta otra lista mal formada en la que lista.cola no apunta al ´ultimo elemento: lista cabeza cola 1 info sig Ya estamos en condiciones de presentar la funci´on: lista cabeza cola.c 1 struct Lista_cc inserta_por_cabeza(struct Lista_cc lista, int valor) 2 { 3 struct Nodo * nuevo; 4 5 nuevo = malloc(sizeof(struct Nodo)); 6 nuevo->info = valor; 7 nuevo->sig = lista.cabeza; 8 if (lista.cabeza == NULL) 9 lista.cola = nuevo; 10 lista.cabeza = nuevo; 11 return lista; 12 } 4.7.3. Inserci´on de nodo en cola La inserci´on de un nodo en cola no es mucho m´as complicada. Como sabemos siempre cu´al es el ´ultimo elemento de la lista, no hemos de buscarlo con un bucle. El procedimiento a seguir es ´este: 1. Pedimos memoria para un nuevo nodo apuntado por un puntero nuevo, nuevo info sig lista cabeza cola 3 info sig 8 info sig 2 info sig 2. asignamos un valor a nuevo->info y hacemos que el nuevo->sig sea NULL nuevo 1 info sig lista cabeza cola 3 info sig 8 info sig 2 info sig 3. hacemos que lista.cola->sig apunte a nuevo, nuevo 1 info sig lista cabeza cola 3 info sig 8 info sig 2 info sig 4. y actualizamos lista.cola para que pase a apuntar a nuevo. 296 Introducci´on a la Programaci´on con C
  • 303. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica nuevo 1 info sig lista cabeza cola 3 info sig 8 info sig 2 info sig Reordenando el gr´afico tenemos: nuevo lista cabeza cola 3 info sig 8 info sig 2 info sig 1 info sig La ´unica precauci´on que hemos de tener es que, cuando la lista est´e inicialmente vac´ıa, se modifique tanto el puntero a la cabeza como el puntero a la cola para que ambos apunten a nuevo. lista cabeza cola.c 1 struct Lista_cc inserta_por_cola(struct Lista_cc lista, int valor) 2 { 3 struct Nodo * nuevo; 4 5 nuevo = malloc(sizeof(struct Nodo)); 6 nuevo->info = valor; 7 nuevo->sig = NULL; 8 9 if (lista.cola != NULL) { 10 lista.cola->sig = nuevo; 11 lista.cola = nuevo; 12 } 13 else 14 lista.cabeza = lista.cola = nuevo; 15 return lista; 16 } F´ıjate: la inserci´on por cola en este tipo de listas es tan eficiente como la inserci´on por cabeza. No importa lo larga que sea la lista: siempre cuesta lo mismo insertar por cola, una cantidad constante de tiempo. Acaba de rendir su primer fruto el contar con punteros a cabeza y cola. 4.7.4. Borrado de la cabeza Eliminar un elemento de la cabeza ha de resultar sencillo con la experiencia adquirida: 1. Si la lista est´a vac´ıa, no hacemos nada. 2. Si la lista tiene un s´olo elemento, lo eliminamos y ponemos lista.cabeza y lista.cola a NULL. 3. Y si la lista tiene m´as de un elemento, como ´esta: lista cabeza cola 3 info sig 8 info sig 2 info sig seguimos este proceso: a) Mantenemos un puntero auxiliar apuntando a la actual cabeza, aux lista cabeza cola 3 info sig 8 info sig 2 info sig Introducci´on a la Programaci´on con C 297
  • 304. 4.7 Listas simples con punteros a cabeza y cola b) hacemos que lista.cabeza apunte al sucesor de la cabeza actual, aux lista cabeza cola 3 info sig 8 info sig 2 info sig c) y liberamos la memoria ocupada por el primer nodo. aux lista cabeza cola 8 info sig 2 info sig lista cabeza cola.c 1 struct Lista_cc borra_cabeza(struct Lista_cc lista) 2 { 3 struct Nodo * aux; 4 5 /* Lista vac´ıa: nada que borrar. */ 6 if (lista.cabeza == NULL) 7 return lista; 8 9 /* Lista con un solo nodo: se borra el nodo y la cabeza y la cola pasan a ser NULL. */ 10 if (lista.cabeza == lista.cola) { 11 free(lista.cabeza); 12 lista.cabeza = lista.cola = NULL; 13 return lista; 14 } 15 16 /* Lista con m´as de un elemento. */ 17 aux = lista.cabeza; 18 lista.cabeza = aux->sig; 19 free(aux); 20 return lista; 21 } 4.7.5. Borrado de la cola El borrado del ´ultimo elemento de una lista con punteros a cabeza y cola plantea un pro- blema: cuando hayamos eliminado el nodo apuntado por lista.cola, ¿a qui´en debe apuntar lista.cola? Naturalmente, al que hasta ahora era el pen´ultimo nodo. ¿Y c´omo sabemos cu´al era el pen´ultimo? S´olo hay una forma de saberlo: busc´andolo con un recorrido de los nodos de la lista. Nuevamente distinguiremos tres casos distintos en funci´on de la talla de la lista: 1. Si la lista est´a vac´ıa, no hacemos nada. 2. Si la lista tiene un ´unico elemento, liberamos su memoria y hacemos que los punteros a cabeza y cola apunten a NULL. 3. En otro caso, actuaremos como en este ejemplo, lista cabeza cola 3 info sig 8 info sig 2 info sig a) buscamos el pen´ultimo elemento (sabremos cu´al es porque si se le apunta con aux, entonces aux->sig coincide con lista.cola) y lo apuntamos con una variable auxiliar aux, 298 Introducci´on a la Programaci´on con C
  • 305. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica aux lista cabeza cola 3 info sig 8 info sig 2 info sig b) hacemos que el pen´ultimo no tenga siguiente nodo (ponemos su campo sig a NULL) para que as´ı pase a ser el ´ultimo, aux lista cabeza cola 3 info sig 8 info sig 2 info sig c) liberamos la memoria del que hasta ahora era el ´ultimo nodo (el apuntado por lista.cola) aux lista cabeza cola 3 info sig 8 info sig d) y, finalmente, hacemos que lista.cola apunte a aux. aux lista cabeza cola 3 info sig 8 info sig lista cabeza cola.c 1 struct Lista_cc borra_cola(struct Lista_cc lista) 2 { 3 struct Nodo * aux; 4 5 /* Lista vac´ıa. */ 6 if (lista.cabeza == NULL) 7 return lista; 8 9 /* Lista con un solo nodo. */ 10 if (lista.cabeza == lista.cola) { 11 free(lista.cabeza); 12 lista.cabeza = lista.cola = NULL; 13 return lista; 14 } 15 16 /* Lista con m´as de un nodo. */ 17 for (aux = lista.cabeza; aux->sig != lista.cola ; aux = aux->sig) ; 18 aux->sig = NULL; 19 free(lista.cola); 20 lista.cola = aux; 21 return lista; 22 } F´ıjate en la condici´on del bucle: detecta si hemos llegado o no al pen´ultimo nodo preguntando si el que sigue a aux es el ´ultimo (el apuntado por lista.cola). La operaci´on de borrado de la cola no es, pues, tan eficiente como la de borrado de la cabeza, pese a que tenemos un puntero a la cola. El tiempo que necesita es directamente proporcional a la longitud de la lista. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 273 Dise˜na una funci´on que determine si un n´umero pertenece o no a una lista con punteros a cabeza y cola. Introducci´on a la Programaci´on con C 299
  • 306. 4.8 Listas con enlace doble · 274 Dise˜na una funci´on que elimine el primer nodo con un valor dado en una lista con punteros a cabeza y cola. · 275 Dise˜na una funci´on que elimine todos los nodos con un valor dado en una lista con punteros a cabeza y cola. · 276 Dise˜na una funci´on que devuelva el elemento que ocupa la posici´on n en una lista con puntero a cabeza y cola. (La cabeza ocupa la posici´on 0.) La funci´on devolver´a como valor de retorno 1 o 0 para, respectivamente, indicar si la operaci´on se pudo completar con ´exito o si fracas´o. La operaci´on no se puede completar con ´exito si n es negativo o si n es mayor o igual que la talla de la lista. El valor del nodo se devolver´a en un par´ametro pasado por referencia. · 277 Dise˜na una funci´on que devuelva un ((corte)) de la lista. Se recibir´an dos ´ındices i y j y se devolver´a una nueva lista con punteros a cabeza y cola con una copia de los nodos que van del que ocupa la posici´on i al que ocupa la posici´on j − 1, ambos inclu´ıdos. La lista devuelta tendr´a punteros a cabeza y cola. · 278 Dise˜na una funci´on de inserci´on ordenada en una lista ordenada con punteros a cabeza y cola. · 279 Dise˜na una funci´on que devuelva el menor valor de una lista ordenada con punteros a cabeza y cola. · 280 Dise˜na una funci´on que devuelva el mayor valor de una lista ordenada con punteros a cabeza y cola. · 281 Dise˜na una funci´on que a˜nada a una lista con punteros a cabeza y cola una copia de otra lista con punteros a cabeza y cola. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.8. Listas con enlace doble Vamos a dotar a cada nodo de dos punteros: uno al siguiente nodo en la lista y otro al anterior. Los nodos ser´an variables de este tipo: lista doble.h 1 struct DNodo { 2 int info; // Valor del nodo. 3 struct DNodo * ant; // Puntero al anterior. 4 struct DNodo * sig; // Puntero al siguiente. 5 }; Una lista es un puntero a un struct DNodo (o a NULL). Nuevamente, definiremos un tipo para poner ´enfasis en que un puntero representa a la lista que ((cuelga)) de ´el. 1 typedef struct DNodo * TipoDLista; Aqu´ı tienes una representaci´on gr´afica de una lista doblemente enlazada: lista 3 ant info sig 8 ant info sig 2 ant info sig Observa que cada nodo tiene dos punteros: uno al nodo anterior y otro al siguiente. ¿Qu´e nodo sigue al ´ultimo nodo? Ninguno, o sea, NULL. ¿Y cu´al antecede al primero? Ninguno, es decir, NULL. 4.8.1. Inserci´on por cabeza La inserci´on por cabeza es relativamente sencilla. Tratemos en primer lugar el caso general: la inserci´on por cabeza en una lista no vac´ıa. Por ejemplo, en ´esta: lista 8 ant info sig 2 ant info sig Vamos paso a paso. 300 Introducci´on a la Programaci´on con C
  • 307. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica 1. Empezamos pidiendo memoria para un nuevo nodo: nuevo ant info sig lista 8 ant info sig 2 ant info sig 2. Asignamos el valor que nos indiquen al campo info: nuevo 3 ant info sig lista 8 ant info sig 2 ant info sig 3. Ajustamos sus punteros ant y sig: nuevo 3 ant info sig lista 8 ant info sig 2 ant info sig 4. Ajustamos el puntero ant del que hasta ahora ocupaba la cabeza: nuevo 3 ant info sig lista 8 ant info sig 2 ant info sig 5. Y, finalmente, hacemos que lista apunte al nuevo nodo: nuevo lista 3 ant info sig 8 ant info sig 2 ant info sig El caso de la inserci´on en la lista vac´ıa es trivial: se pide memoria para un nuevo nodo cuyos punteros ant y sig se ponen a NULL y hacemos que la cabeza apunte a dicho nodo. Aqu´ı tienes la funci´on que codifica el m´etodo descrito. Hemos factorizado y dispuesto al principio los elementos comunes al caso general y al de la lista vac´ıa: lista doble.c 1 TipoDLista inserta_por_cabeza(TipoDLista lista, int valor) 2 { 3 struct DNodo * nuevo; 4 5 nuevo = malloc(sizeof(struct DNodo)); 6 nuevo->info = valor; 7 nuevo->ant = NULL; 8 nuevo->sig = lista; 9 10 if (lista != NULL) 11 lista->ant = nuevo; 12 13 lista = nuevo; 14 return lista; 15 } Introducci´on a la Programaci´on con C 301
  • 308. 4.8 Listas con enlace doble Te proponemos como ejercicios algunas de las funciones b´asicas para el manejo de listas doblemente enlazadas: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 282 Dise˜na una funci´on que inserte un nuevo nodo al final de una lista doblemente enlazada. · 283 Dise˜na una funci´on que borre la cabeza de una lista doblemente enlazada. Presta especial atenci´on al caso en el que la lista consta de un s´olo elemento. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.8.2. Borrado de la cola Vamos a desarrollar la funci´on de borrado del ´ultimo elemento de una lista doblemente enlazada, pues presenta alg´un aspecto interesante. Desarrollemos nuevamente el caso general sobre una lista concreta para deducir el m´etodo a seguir. Tomemos, por ejemplo, ´esta: lista 3 ant info sig 8 ant info sig 2 ant info sig 1. Empezamos localizando el ´ultimo elemento de la lista (con un bucle) y apunt´andolo con un puntero: lista 3 ant info sig 8 ant info sig aux 2 ant info sig 2. Y localizamos ahora el pen´ultimo en un s´olo paso (es aux->ant): lista 3 ant info sig atras 8 ant info sig aux 2 ant info sig 3. Se elimina el ´ultimo nodo (el apuntado por aux): lista 3 ant info sig atras 8 ant info sig aux 4. Y se pone el campo sig del que hasta ahora era pen´ultimo (el apuntado por atras) a NULL. lista 3 ant info sig atras 8 ant info sig aux El caso de la lista vac´ıa tiene f´acil soluci´on: no hay nada que borrar. Es m´as problem´atica la lista con s´olo un nodo. El problema con ella estriba en que no hay elemento pen´ultimo (el anterior al ´ultimo es NULL). Tendremos, pues, que detectar esta situaci´on y tratarla adecuadamente. lista doble.c 1 TipoDLista borra_por_cola(TipoDLista lista) 2 { 3 struct DNodo * aux, * atras; 4 5 /* Lista vac´ıa. */ 6 if (lista == NULL) 302 Introducci´on a la Programaci´on con C
  • 309. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica 7 return lista; 8 9 /* Lista con un nodo. */ 10 if (lista->sig == NULL) { 11 free(lista); 12 lista = NULL; 13 return lista; 14 } 15 16 /* Caso general. */ 17 for (aux=lista; aux->sig!=NULL; aux=aux->sig) ; 18 atras = aux->ant; 19 free(aux); 20 atras->sig = NULL; 21 return lista; 22 } 4.8.3. Inserci´on en una posici´on determinada Tratemos ahora el caso de la inserci´on de un nuevo nodo en la posici´on n de una lista doblemente enlazada. lista 3 ant info sig 8 ant info sig 2 ant info sig 0 1 2 3 Si n est´a fuera del rango de ´ındices ((v´alidos)), insertaremos en la cabeza (si n es negativo) o en la cola (si n es mayor que el n´umero de elementos de la lista). A simple vista percibimos ya diferentes casos que requerir´an estrategias diferentes: La lista vac´ıa: la soluci´on en este caso es trivial. Inserci´on al principio de la lista: seguiremos la misma rutina dise˜nada para insertar por cabeza y, por qu´e no, utilizaremos la funci´on que dise˜namos en su momento. Inserci´on al final de la lista: ´ıdem.7 Inserci´on entre dos nodos de una lista. Vamos a desarrollar completamente el ´ultimo caso. Nuevamente usaremos una lista concreta para deducir cada uno de los detalles del m´etodo. Insertaremos el valor 1 en la posici´on 2 de esta lista: lista 3 ant info sig 8 ant info sig 2 ant info sig 1. Empezamos localizando el elemento que ocupa actualmente la posici´on n. Un simple bucle efectuar´a esta labor: lista 3 ant info sig 8 ant info sig aux 2 ant info sig 2. Pedimos memoria para un nuevo nodo, lo apuntamos con el puntero nuevo y le asignamos el valor: lista 3 ant info sig 8 ant info sig aux 2 ant info sig nuevo 1 ant info sig 7Ver m´as adelante el ejercicio 285. Introducci´on a la Programaci´on con C 303
  • 310. 4.8 Listas con enlace doble 3. Hacemos que nuevo->sig sea aux: lista 3 ant info sig 8 ant info sig aux 2 ant info sig nuevo 1 ant info sig 4. Hacemos que nuevo->ant sea aux->ant: lista 3 ant info sig 8 ant info sig aux 2 ant info sig nuevo 1 ant info sig 5. Ojo con este paso, que es complicado. Hacemos que el anterior a aux tenga como siguiente a nuevo, es decir, aux->ant->sig = nuevo: lista 3 ant info sig 8 ant info sig aux 2 ant info sig nuevo 1 ant info sig 6. Y ya s´olo resta que el anterior a aux sea nuevo con la asignaci´on aux->ant = nuevo: lista 3 ant info sig 8 ant info sig aux 2 ant info sig nuevo 1 ant info sig Ahora que tenemos claro el procedimiento, podemos escribir la funci´on: lista doble.c 1 TipoDLista inserta_en_posicion(TipoDLista lista, int pos, int valor) 2 { 3 struct DNodo * aux, * nuevo; 4 int i; 5 6 /* Caso especial: lista vac´ıa */ 7 if (lista == NULL) { 8 lista = inserta_por_cabeza(lista, valor); 9 return lista; 10 } 11 12 /* Inserci´on en cabeza en lista no vac´ıa. */ 13 if (pos <= 0) { 14 lista = inserta_por_cabeza(lista, valor); 15 return lista; 16 } 17 304 Introducci´on a la Programaci´on con C
  • 311. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica 18 /* Inserci´on no en cabeza. */ 19 nuevo = malloc(sizeof(struct DNodo)); 20 nuevo->info = valor; 21 for (i = 0, aux = lista; i < pos && aux != NULL; i++, aux = aux->sig) ; 22 if (aux == NULL) /* Inserci´on por cola. */ 23 lista = inserta_por_cola(lista, valor); 24 else { 25 nuevo->sig = aux; 26 nuevo->ant = aux->ant; 27 aux->ant->sig = nuevo; 28 aux->ant = nuevo; 29 } 30 return lista; 31 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 284 Reescribe la funci´on de inserci´on en una posici´on dada para que no efect´ue llamadas a la funci´on inserta_por_cabeza. · 285 Reescribe la funci´on de inserci´on en una posici´on dada para que no efect´ue llamadas a la funci´on inserta_por_cola. ¿Es m´as eficiente la nueva versi´on? ¿Por qu´e? · 286 ¿Qu´e ocurrir´ıa si las ´ultimas l´ıneas de la funci´on fueran ´estas?: 1 ... 2 nuevo->sig = aux; 3 nuevo->ant = aux->ant; 4 aux->ant = nuevo; 5 aux->ant->sig = nuevo; 6 } 7 return lista; 8 } ¿Es correcta ahora la funci´on? Haz una traza con un caso concreto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.8.4. Borrado de la primera aparici´on de un elemento Nuevamente hay un par de casos triviales: si la lista est´a vac´ıa, no hay que hacer nada y si la lista tiene un s´olo elemento, s´olo hemos de actuar si ese elemento tiene el valor buscado, en cuyo caso liberaremos la memoria del nodo en cuesti´on y convertiremos la lista en una lista vac´ıa. Desarrollemos un caso general. Supongamos que en esta lista hemos de eliminar el primer y ´unico nodo con valor 8: lista 3 ant info sig 8 ant info sig 2 ant info sig Vamos paso a paso: 1. Empezamos por localizar el elemento y apuntarlo con un puntero auxiliar aux: lista 3 ant info sig aux 8 ant info sig 2 ant info sig 2. Hacemos que el que sigue al anterior de aux sea el siguiente de aux (¡qu´e galimat´ıas!). O sea, hacemos aux->ant->sig=aux->sig: lista 3 ant info sig aux 8 ant info sig 2 ant info sig Introducci´on a la Programaci´on con C 305
  • 312. 4.8 Listas con enlace doble 3. Ahora hacemos que el que antecede al siguiente de aux sea el anterior a aux. Es decir, aux->sig->ant=aux->ant: lista 3 ant info sig aux 8 ant info sig 2 ant info sig 4. Y ya podemos liberar la memoria ocupada por el nodo apuntado con aux: lista 3 ant info sig aux 2 ant info sig Hemos de ser cautos. Hay un par de casos especiales que merecen ser tratados aparte: el borrado del primer nodo y el borrado del ´ultimo nodo. Veamos c´omo proceder en el primer caso: tratemos de borrar el nodo de valor 3 en la lista del ejemplo anterior. 1. Una vez apuntado el nodo por aux, sabemos que es el primero porque apunta al mismo nodo que lista: aux lista 3 ant info sig 8 ant info sig 2 ant info sig 2. Hacemos que el segundo nodo deje de tener antecesor, es decir, que el puntero aux->sig->ant valga NULL (que, por otra parte, es lo mismo que hacer aux->sig->ant=aux->ant): aux lista 3 ant info sig 8 ant info sig 2 ant info sig 3. Ahora hacemos que lista pase a apuntar al segundo nodo (lista=aux->sig): aux lista 3 ant info sig 8 ant info sig 2 ant info sig 4. Y por fin, podemos liberar al nodo apuntado por aux (free(aux)): aux lista 8 ant info sig 2 ant info sig Vamos a por el caso en que borramos el ´ultimo elemento de la lista: 1. Empezamos por localizarlo con aux y detectamos que efectivamente es el ´ultimo porque aux->sig es NULL: 306 Introducci´on a la Programaci´on con C
  • 313. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica aux lista 3 ant info sig 8 ant info sig 2 ant info sig 2. Hacemos que el siguiente del que antecede a aux sea NULL: (aux->ant->sig=NULL): aux lista 3 ant info sig 8 ant info sig 2 ant info sig 3. Y liberamos el nodo apuntado por aux: aux lista 3 ant info sig 8 ant info sig lista doble.c 1 TipoDLista borra_primera_ocurrencia(TipoDLista lista, int valor) 2 { 3 struct DNodo * aux; 4 5 for (aux=lista; aux!=NULL; aux=aux->sig) 6 if (aux->info == valor) 7 break; 8 9 if (aux == NULL) // No se encontr´o. 10 return lista; 11 12 if (aux->ant == NULL) // Es el primero de la lista. 13 lista = aux->sig; 14 else 15 aux->ant->sig = aux->sig; 16 17 if (aux->sig != NULL) // No es el ´ultimo de la lista. 18 aux->sig->ant = aux->ant; 19 20 free(aux); 21 22 return lista; 23 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 287 Dise˜na una funci´on que permita efectuar la inserci´on ordenada de un elemento en una lista con enlace doble que est´a ordenada. · 288 Dise˜na una funci´on que permita concatenar dos listas doblemente enlazadas. La funci´on recibir´a las dos listas y devolver´a una lista nueva con una copia de la primera seguida de una copia de la segunda. · 289 Dise˜na una funci´on que devuelva una copia invertida de una lista doblemente enlazada. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.9. Listas con enlace doble y puntero a cabeza y cola Ya sabemos manejar listas con puntero a cabeza y listas con punteros a cabeza y cola. Hemos visto que las listas con puntero a cabeza son ineficientes a la hora de a˜nadir elementos por la cola: se tarda tanto m´as cuanto mayor es el n´umero de elementos de la lista. Las listas Introducci´on a la Programaci´on con C 307
  • 314. 4.9 Listas con enlace doble y puntero a cabeza y cola con puntero a cabeza y cola permiten realizar operaciones de inserci´on por cola en un n´umero constante de pasos. A´un as´ı, hay operaciones de cola que tambi´en son ineficientes en esta ´ultima estructura de datos: la eliminaci´on del nodo de cola, por ejemplo, sigue necesitando un tiempo proporcional a la longitud de la lista. La estructura que presentamos en esta secci´on, la lista doblemente enlazada con puntero a cabeza y cola, corrige la ineficiencia en el borrado del nodo de cola. Una lista doblemente enlazada con puntero a cabeza y cola puede representarse gr´aficamente as´ı: lista cabeza cola 3 ant info sig 8 ant info sig 2 ant info sig La definici´on del tipo es f´acil ahora que ya hemos estudiado diferentes tipos de listas: lista doble cc.h 1 struct DNodo { 2 int info; 3 struct DNodo * ant; 4 struct DNodo * sig; 5 }; 6 7 struct DLista_cc { 8 struct DNodo * cabeza; 9 struct DNodo * cola; 10 } 11 12 typedef struct DLista_cc TipoDListaCC; S´olo vamos a presentarte una de las operaciones sobre este tipo de listas: el borrado de la cola. El resto de operaciones te las proponemos como ejercicios. Con cualquiera de las otras estructuras de datos basadas en registros enlazados, el borrado del nodo de cola no pod´ıa efectuarse en tiempo constante. ´Esta lo hace posible. ¿C´omo? Lo mejor es que, una vez m´as, despleguemos los diferentes casos y estudiemos ejemplos concretos cuando convenga: Si la lista est´a vac´ıa, no hay que hacer nada. Si la lista tiene un solo elemento, liberamos su memoria y ponemos los punteros a cabeza y cola a NULL. Y si la lista tiene m´as de un elemento, como ´esta: lista cabeza cola 3 ant info sig 8 ant info sig 2 ant info sig hacemos lo siguiente: a) localizamos al pen´ultimo elemento, que es lista.cola->ant, y lo mantenemos apuntado con un puntero auxiliar aux: aux lista cabeza cola 3 ant info sig 8 ant info sig 2 ant info sig b) liberamos la memoria apuntada por lista.cola: aux lista cabeza cola 3 ant info sig 8 ant info sig 308 Introducci´on a la Programaci´on con C
  • 315. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica c) ponemos aux->sig a NULL: aux lista cabeza cola 3 ant info sig 8 ant info sig d) y ajustamos lista.cola para que apunte ahora donde apunta aux: aux lista cabeza cola 3 ant info sig 8 ant info sig Ya podemos escribir el programa: lista doble cc.c 1 TipoDListaCC borra_cola(TipoDListaCC lista) 2 { 3 if (lista.cabeza == NULL) 4 return lista; 5 6 if (lista.cabeza == lista.cola) { 7 free(lista.cabeza); 8 lista.cabeza = lista.cola = NULL; 9 return lista; 10 } 11 12 aux = lista.cola->ant; 13 free(lista.cola); 14 aux->sig = NULL; 15 lista.cola = aux; 16 return lista; 17 } Ha sido f´acil, ¿no? No ha hecho falta bucle alguno. La operaci´on se ejecuta en un n´umero de pasos que es independiente de lo larga que sea la lista. Ahora te toca a t´ı desarrollar c´odigo. Practica con estos ejercicios: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 290 Dise˜na una funci´on que calcule la longitud de una lista doblemente enlazada con pun- teros a cabeza y cola. · 291 Dise˜na una funci´on que permita insertar un nuevo nodo en cabeza. · 292 Dise˜na una funci´on que permita insertar un nuevo nodo en cola. · 293 Dise˜na una funci´on que permita borrar el nodo de cabeza. · 294 Dise˜na una funci´on que elimine el primer elemento de la lista con un valor dado. · 295 Dise˜na una funci´on que elimine todos los elementos de la lista con un valor dado. · 296 Dise˜na una funci´on que inserte un nodo en una posici´on determinada que se indica por su ´ındice. · 297 Dise˜na una funci´on que inserte ordenadamente en una lista ordenada. · 298 Dise˜na una funci´on que muestre por pantalla el contenido de una lista, mostrando el valor de cada celda en una l´ınea. Los elementos se mostrar´an en el mismo orden con el que aparecen en la lista. · 299 Dise˜na una funci´on que muestre por pantalla el contenido de una lista, mostrando el valor de cada celda en un l´ınea. Los elementos se mostrar´an en orden inverso. · 300 Dise˜na una funci´on que devuelva una copia invertida de una lista doblemente enlazada con puntero a cabeza y cola. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Introducci´on a la Programaci´on con C 309
  • 316. 4.10 Una gu´ıa para elegir listas 4.10. Una gu´ıa para elegir listas Hemos estudiado cuatro tipos diferentes de listas basadas en registros enlazados. ¿Por qu´e tan- tas? Porque cada una supone una soluci´on de compromiso diferente entre velocidad y consumo de memoria. Empecemos por estudiar el consumo de memoria. Supongamos que una variable del tipo del campo info ocupa m bytes, que cada puntero ocupa 4 bytes y que la lista consta de n elementos. Esta tabla muestra la ocupaci´on en bytes seg´un la estructura de datos escogida: lista con enlace simple (((simple))), lista con enlace simple y puntero a cabeza y cola (((simple cabeza/cola))), lista con enlace doble (((doble))), lista con enlace doble y puntero a cabeza y cola (((doble cabeza/cola))). memoria (bytes) simple 4 + n · (4 + m) simple cabeza/cola 8 + n · (4 + m) doble 4 + n · (8 + m) doble cabeza/cola 8 + n · (8 + m) Esta otra tabla resume el tiempo que requieren algunas operaciones sobre los cuatro tipos de lista: simple simple cabeza/cola doble doble cabeza/cola Insertar por cabeza constante constante constante constante Borrar cabeza constante constante constante constante Insertar por cola lineal constante lineal constante Borrar cola lineal lineal lineal constante Buscar un nodo concreto lineal∗ lineal∗ lineal∗ lineal∗ Invertir la lista cuadr´atico cuadr´atico lineal lineal Hemos indicado con la palabra ((constante)) que se requiere una cantidad de tiempo fija, independiente de la longitud de la lista; con la palabra ((lineal)), que se requiere un tiempo que es proporcional a la longitud de la lista; y con ((cuadr´atico)), que el coste crece con el cuadrado del n´umero de elementos. Para que te hagas una idea: insertar por cabeza un nodo en una lista cuesta siempre la misma cantidad de tiempo, tenga la lista 100 o 1000 nodos. Insertar por la cola en una lista simplemente enlazada con puntero a cabeza, sin embargo, es unas 10 veces m´as lento si la lista es 10 veces m´as larga. Esto no ocurre con una lista simplemente enlazada que tenga puntero a cabeza y cola: insertar por la cola en ella siempre cuesta lo mismo. ¡Ojo con los costes cuadr´aticos! Invertir una lista simplemente enlazada de 1000 elementos es 100 veces m´as costoso que invertir una lista con 10 veces menos elementos. En la tabla hemos marcado algunos costes con un asterisco. Son costes para el peor de los casos. Buscar un nodo concreto en una lista obliga a recorrer todos los nodos s´olo si el que buscamos no est´a o si ocupa la ´ultima posici´on. En el mejor de los casos, el coste temporal es constante: ello ocurre cuando el nodo buscado se encuentra en la lista y, adem´as, ocupa la primera posici´on. De los an´alisis de coste nos ocuparemos m´as adelante. Un an´alisis de la tabla de tiempos permite concluir que la lista doblemente enlazada con punteros a cabeza y cola es siempre igual o mejor que las otras estructuras. ¿Debemos escogerla siempre? No, por tres razones: 1. Aunque la lista m´as compleja requiere tiempo constante en muchas operaciones, ´estas son algo m´as lentas y sofisticadas que operaciones an´alogas en las otras estructuras m´as sencillas. Son, por fuerza, algo m´as lentas. 2. El consumo de memoria es mayor en la lista m´as compleja (8 bytes adicionales para cada nodo y 8 bytes para los punteros a cabeza y cola, frente a 4 bytes adicionales para cada nodo y 4 bytes para un puntero a cabeza en la estructura m´as sencilla), as´ı que puede no compensar la ganancia en velocidad o, sencillamente, es posible que no podamos permitirnos el lujo de gastar el doble de memoria extra. 3. Puede que nuestra aplicaci´on s´olo efect´ue operaciones ((baratas)) sobre cualquier lista. Ima- gina que necesitas una lista en la que siempre insertas y eliminas nodos por cabeza, jam´as por el final. Las cuatro estructuras ofrecen tiempo constante para esas dos operaciones, s´olo que, adem´as, las dos primeras son mucho m´as sencillas y consumen menos memoria que las dos ´ultimas. 310 Introducci´on a la Programaci´on con C
  • 317. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 301 Rellena una tabla similar a la anterior para estas otras operaciones: a) Insertar ordenadamente en una lista ordenada. b) Insertar en una posici´on concreta. c) Buscar un elemento en una lista ordenada. d) Buscar el elemento de valor m´ınimo en una lista ordenada. e) Buscar el elemento de valor m´aximo en una lista ordenada. f) Unir dos listas ordenadas de modo que el resultado est´e ordenado. g) Mostrar el contenido de una lista por pantalla. h) Mostrar el contenido de una lista en orden inverso por pantalla. · 302 Vamos a montar una pila con listas. La pila es una estructura de datos en la que s´olo podemos efectuar las siguientes operaciones: insertar un elemento en la cima, eliminar el elemento de la cima, consultar el valor del elemento de la cima. ¿Qu´e tipo de lista te parece m´as adecuado para implementar una pila? ¿Por qu´e? · 303 Vamos a montar una cola con listas. La cola es una estructura de datos en la que s´olo podemos efectuar las siguientes operaciones: insertar un elemento al final de la cola, eliminar el elemento que hay al principio de la cola, consultar el valor del elemento que hay al principio de la cola. ¿Qu´e tipo de lista te parece m´as adecuado para construir una cola? ¿Por qu´e? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.11. Una aplicaci´on: una base de datos para discos compactos En este apartado vamos a desarrollar una aplicaci´on pr´actica que usa listas: un programa para la gesti´on de una colecci´on de discos compactos. Cada disco compacto contendr´a un t´ıtulo, un int´erprete, un a˜no de edici´on y una lista de canciones. De cada canci´on nos interesar´a ´unicamente el t´ıtulo. Las acciones del programa, que se presentar´an al usuario con un men´u, son ´estas. 1. A˜nadir un disco. 2. Buscar discos por t´ıtulo. 3. Buscar discos por int´erprete. 4. Buscar discos por t´ıtulo de canci´on. 5. Mostrar el contenido completo de la colecci´on. 6. Eliminar un disco de la base de datos dados su t´ıtulo y el nombre del int´erprete. 7. Finalizar. Introducci´on a la Programaci´on con C 311
  • 318. 4.11 Una aplicaci´on: una base de datos para discos compactos A priori no sabemos cu´antas canciones hay en un disco, ni cu´antos discos hay que almacenar en la base de datos, as´ı que utilizaremos listas para ambas entidades. Nuestra colecci´on ser´a, pues, una lista de discos que, a su vez, contienen listas de canciones. No s´olo eso: no queremos que nuestra aplicaci´on desperdicie memoria con cadenas que consumen m´as memoria que la necesaria, as´ı que usaremos memoria din´amica tambi´en para la reserva de memoria para cadenas. Lo mejor es dividir el problema en estructuras de datos claramente diferenciadas (una para la lista de discos y otra para la lista de canciones) y dise˜nar funciones para manejar cada una de ellas. Atenci´on al montaje que vamos a presentar, pues es el m´as complicado de cuantos hemos estudiado. 1 struct Cancion { 2 char * titulo; 3 struct Cancion * sig; 4 }; 5 6 typedef struct Cancion * TipoListaCanciones; 7 8 struct Disco { 9 char * titulo; 10 char * interprete; 11 int anyo; 12 TipoListaCanciones canciones; 13 struct Disco * sig; 14 }; 15 16 typedef struct Disco * TipoColeccion; Hemos optado por listas simplemente enlazadas y con puntero a cabeza. Aqu´ı tienes una representaci´on gr´afica de una colecci´on con 3 discos compactos: coleccion 1972 titulo interprete anyo canciones sig 1982 titulo interprete anyo canciones sig 1977 titulo interprete anyo canciones sig titulo sig titulo sig titulo sig titulo sig titulo sig titulo sig titulo sig titulo sig I 0 g 1 n 2 a 3 c 4 i 5 o 6 0 7 L 0 o 1 g 2 o 3 s 4 0 5 D 0 o 1 m 2 i 3 n 4 i 5 o 6 n 7 0 8 O 0 g 1 u 2 n 3 d 4 e 5 0 6 T 0 o 1 2 b 3 e 4 0 5 O 0 f 1 f 2 e 3 r 4 i 5 n 6 g 7 0 8 E 0 x 1 p 2 r 3 e 4 s 5 s 6 i 7 o 8 n 9 0 10 N 0 u 1 m 2 b 3 e 4 r 5 6 o 7 n 8 e 9 0 10 E 0 x 1 p 2 r 3 e 4 s 5 s 6 i 7 o 8 n 9 0 10 J 0 o 1 h 2 n 3 4 C 5 o 6 l 7 t 8 r 9 a 10 n 11 e 12 0 13 L 0 o 1 g 2 o 3 s 4 0 5 T 0 a 1 n 2 g 3 e 4 r 5 i 6 n 7 e 8 9 D 10 r 11 e 12 a 13 m 14 0 15 I 0 g 1 n 2 a 3 c 4 i 5 o 6 0 7 V 0 a 1 n 2 g 3 e 4 l 5 i 6 s 7 0 8 Empezaremos por dise˜nar la estructura que corresponde a una lista de canciones. Despu´es nos ocuparemos del dise˜no de registros del tipo ((disco compacto)). Y acabaremos definiendo un tipo ((colecci´on de discos compactos)). 312 Introducci´on a la Programaci´on con C
  • 319. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica Vamos a dise˜nar funciones para gestionar listas de canciones. Lo que no vamos a hacer es montar toda posible operaci´on sobre una lista. S´olo invertiremos esfuerzo en las operaciones que se van a utilizar. ´Estas son: Crear una lista vac´ıa. A˜nadir una canci´on a la lista. (Durante la creaci´on de un disco iremos pidiendo las can- ciones y a˜nadi´endolas a la ficha del disco.) Mostrar la lista de canciones por pantalla. (Esta funci´on se usar´a cuando se muestre una ficha detallada de un disco.) Buscar una canci´on y decir si est´a o no est´a en la lista. Borrar todas las canciones de la lista. (Cuando se elimine un disco de la base de datos tendremos que liberar la memoria ocupada por todas sus canciones.) La funci´on de creaci´on de una lista de canciones es trivial: 1 TipoListaCanciones crea_lista_canciones(void) 2 { 3 return NULL; 4 } Pasemos a la funci´on que a˜nade una canci´on a una lista de canciones. No nos indican que las canciones deban almacenarse en un orden determinado, as´ı que recurriremos al m´etodo m´as sencillo: la inserci´on por cabeza. 1 TipoListaCanciones anyade_cancion(TipoListaCanciones lista, char titulo[]) 2 { 3 struct Cancion * nuevo = malloc(sizeof(struct Cancion)); 4 5 nuevo->titulo = malloc((strlen(titulo)+1)*sizeof(char)); 6 strcpy(nuevo->titulo, titulo); 7 nuevo->sig = lista; 8 lista = nuevo; 9 return lista; 10 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 304 La verdad es que insertar las canciones por la cabeza es el m´etodo menos indicado, pues cuando se recorra la lista para mostrarlas por pantalla aparecer´an en orden inverso a aqu´el con el que fueron introducidas. Modifica anyade_cancion para que las canciones se inserten por la cola. · 305 Y ya que sugerimos que insertes canciones por cola, modifica las estructuras necesarias para que la lista de canciones se gestione con una lista de registros con puntero a cabeza y cola. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Mostrar la lista de canciones es muy sencillo: 1 void muestra_canciones(TipoListaCanciones lista) 2 { 3 struct Cancion * aux; 4 5 for (aux=lista; aux!=NULL; aux=aux->sig) 6 printf (" %sn", aux->titulo); 7 } Buscar una canci´on es un simple recorrido que puede terminar anticipadamente tan pronto se encuentra el objeto buscado: 1 int contiene_cancion_con_titulo(TipoListaCanciones lista, char titulo[]) 2 { 3 struct Cancion * aux; 4 5 for (aux=lista; aux!=NULL; aux=aux->sig) Introducci´on a la Programaci´on con C 313
  • 320. 4.11 Una aplicaci´on: una base de datos para discos compactos 6 if (strcmp(aux->titulo, titulo)==0) 7 return 1; 8 return 0; 9 } Borrar todas las canciones de una lista debe liberar la memoria propia de cada nodo, pero tambi´en debe liberar la cadena que almacena cada t´ıtulo, pues tambi´en se solicit´o con malloc: 1 TipoListaCanciones libera_canciones(TipoListaCanciones lista) 2 { 3 struct Cancion * aux, * siguiente; 4 5 aux = lista; 6 while (aux != NULL) { 7 siguiente = aux->sig; 8 free(aux->titulo); 9 free(aux); 10 aux = siguiente; 11 } 12 return NULL; 13 } No ha sido tan dif´ıcil. Una vez sabemos manejar listas, las aplicaciones pr´acticas se dise˜nan reutilizando buena parte de las rutinas que hemos presentado en apartados anteriores. Pasamos a encargarnos de las funciones que gestionan la lista de discos. Como es habitual, empezamos con una funci´on que crea una colecci´on (una lista) vac´ıa: 1 TipoColeccion crea_coleccion(void) 2 { 3 return NULL; 4 } A˜nadir un disco obliga a solicitar memoria tanto para el registro en s´ı como para algunos de sus componentes: el t´ıtulo y el int´erprete: 1 TipoColeccion anyade_disco(TipoColeccion lista, char titulo[], char interprete[], 2 int anyo, TipoListaCanciones canciones) 3 { 4 struct Disco * disco; 5 6 disco = malloc(sizeof(struct Disco)); 7 disco->titulo = malloc((strlen(titulo)+1)*sizeof(char)); 8 strcpy(disco->titulo, titulo); 9 disco->interprete = malloc((strlen(interprete)+1)*sizeof(char)); 10 strcpy(disco->interprete, interprete); 11 disco->anyo = anyo; 12 disco->canciones = canciones; 13 disco->sig = lista; 14 lista = disco; 15 return lista; 16 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 306 Modifica anyade_disco para que los discos est´en siempre ordenados alfab´eticamente por int´erprete y, para cada int´erprete, por valor creciente del a˜no de edici´on. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Y la memoria solicitada debe liberarse ´ıntegramente: si al reservar memoria para un disco ejecutamos tres llamadas a malloc, habr´a que efectuar tres llamadas a free: 1 TipoColeccion libera_coleccion(TipoColeccion lista) 2 { 3 struct Disco * aux, * siguiente; 4 5 aux = lista; 6 while (aux != NULL) { 314 Introducci´on a la Programaci´on con C
  • 321. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica 7 siguiente = aux->sig; 8 free(aux->titulo); 9 free(aux->interprete); 10 aux->canciones = libera_canciones(aux->canciones); 11 free(aux); 12 aux = siguiente; 13 } 14 return NULL; 15 } Mostrar por pantalla el contenido de un disco es sencillo, especialmente si usamos mues- tra_canciones para mostrar la lista de canciones. 1 void muestra_disco(struct Disco eldisco) 2 { 3 printf ("T´ıtulo: %sn", eldisco.titulo); 4 printf ("Int´erprete: %sn", eldisco.interprete); 5 printf ("A~no de edici´on: %dn", eldisco.anyo); 6 printf ("Canciones:n"); 7 muestra_canciones(eldisco.canciones); 8 } Mostrar la colecci´on completa es trivial si usamos la funci´on que muestra un disco: 1 void muestra_coleccion(TipoColeccion lista) 2 { 3 struct Disco * aux; 4 5 for (aux=lista; aux!=NULL; aux=aux->sig) 6 muestra_disco(*aux); 7 } Las funciones de b´usqueda de discos se usan en un contexto determinado: el de mostrar, si se encuentra el disco, su contenido por pantalla. En lugar de hacer que la funci´on devuelva el valor 1 o 0, podemos hacer que devuelva un puntero al registro cuando lo encuentre o NULL cuando el disco no est´e en la base de datos. Aqu´ı tienes las funciones de b´usqueda por t´ıtulo y por int´erprete: 1 struct Disco * busca_disco_por_titulo_disco(TipoColeccion coleccion, char titulo[]) 2 { 3 struct Disco * aux; 4 5 for (aux=coleccion; aux!=NULL; aux=aux->sig) 6 if (strcmp(aux->titulo, titulo) == 0) 7 return aux; 8 return NULL; 9 } 10 11 struct Disco * busca_disco_por_interprete(TipoColeccion coleccion, char interprete[]) 12 { 13 struct Disco * aux; 14 15 for (aux=coleccion; aux!=NULL; aux=aux->sig) 16 if (strcmp(aux->interprete, interprete) == 0) 17 return aux; 18 return NULL; 19 } La funci´on de b´usqueda por t´ıtulo de canci´on es similar, s´olo que llama a la funci´on que busca una canci´on en una lista de canciones: 1 struct Disco * busca_disco_por_titulo_cancion(TipoColeccion coleccion, char titulo[]) 2 { 3 struct Disco * aux; 4 5 for (aux=coleccion; aux!=NULL; aux=aux->sig) Introducci´on a la Programaci´on con C 315
  • 322. 4.11 Una aplicaci´on: una base de datos para discos compactos 6 if (contiene_cancion_con_titulo(aux->canciones, titulo)) 7 return aux; 8 return NULL; 9 } S´olo nos queda por definir la funci´on que elimina un disco de la colecci´on dado su t´ıtulo: 1 TipoColeccion borra_disco_por_titulo_e_interprete(TipoColeccion coleccion, char titulo[], 2 char interprete[]) 3 { 4 struct Disco *aux, *atras; 5 6 for (atras = NULL, aux=coleccion; aux != NULL; atras = aux, aux = aux->sig) 7 if (strcmp(aux->titulo, titulo) == 0 && strcmp(aux->interprete, interprete) == 0) { 8 if (atras == NULL) 9 coleccion = aux->sig; 10 else 11 atras->sig = aux->sig; 12 free(aux->titulo); 13 free(aux->interprete); 14 aux->canciones = libera_canciones(aux->canciones); 15 free(aux); 16 return coleccion; 17 } 18 return coleccion; 19 } Ya tenemos todas las herramientas para enfrentarnos al programa principal: discoteca2.c discoteca2.c 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <ctype.h> 5 6 #define MAXCAD 1000 7 8 enum { Anyadir=1, BuscarPorTituloDisco, BuscarPorInterprete, BuscarPorTituloCancion, 9 Mostrar, EliminarDisco, Salir}; 10 . . . 182 183 int main(void) 184 { 185 int opcion; 186 TipoColeccion coleccion; 187 char titulo_disco[MAXCAD+1], titulo_cancion[MAXCAD+1], interprete[MAXCAD+1]; 188 char linea[MAXCAD+1]; 189 int anyo; 190 struct Disco * undisco; 191 TipoListaCanciones lista_canciones; 192 193 coleccion = crea_coleccion(); 194 195 do { 196 printf ("Men´un"); 197 printf ("1) A~nadir discon"); 198 printf ("2) Buscar por t´ıtulo del discon"); 199 printf ("3) Buscar por int´erpreten"); 200 printf ("4) Buscar por t´ıtulo de canci´onn"); 201 printf ("5) Mostrar todon"); 202 printf ("6) Eliminar un disco por t´ıtulo e int´erprten"); 203 printf ("7) Finalizarn"); 204 printf ("Opci´on: "); gets(linea); sscanf (linea, "%d", &opcion); 316 Introducci´on a la Programaci´on con C
  • 323. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica 205 206 switch(opcion) { 207 case Anyadir: 208 printf ("T´ıtulo: "); gets(titulo_disco); 209 printf ("Int´erprete: "); gets(interprete); 210 printf ("A~no: "); gets(linea); sscanf (linea, "%d", &anyo); 211 lista_canciones = crea_lista_canciones(); 212 do { 213 printf ("T´ıtulo de canci´on (pulse retorno para acabar): "); 214 gets(titulo_cancion); 215 if (strlen(titulo_cancion) > 0) 216 lista_canciones = anyade_cancion(lista_canciones, titulo_cancion); 217 } while (strlen(titulo_cancion) > 0); 218 coleccion = anyade_disco(coleccion, titulo_disco, interprete, anyo, lista_canciones); 219 break; 220 221 case BuscarPorTituloDisco: 222 printf ("T´ıtulo: "); gets(titulo_disco); 223 undisco = busca_disco_por_titulo_disco(coleccion, titulo_disco); 224 if (undisco != NULL) 225 muestra_disco(*undisco); 226 else 227 printf ("No hay discos con t´ıtulo ’%s’n", titulo_disco); 228 break; 229 230 case BuscarPorInterprete: 231 printf ("Int´erprete: "); gets(interprete); 232 undisco = busca_disco_por_interprete(coleccion, interprete); 233 if (undisco != NULL) 234 muestra_disco(*undisco); 235 else 236 printf ("No hay discos de %sn", interprete); 237 break; 238 239 case BuscarPorTituloCancion: 240 printf ("T´ıtulo: "); gets(titulo_cancion); 241 undisco = busca_disco_por_titulo_cancion(coleccion, titulo_cancion); 242 if (undisco != NULL) 243 muestra_disco(*undisco); 244 else 245 printf ("No hay discos con alguna canci´on titulada ’%s’n", titulo_cancion); 246 break; 247 248 case Mostrar: 249 muestra_coleccion(coleccion); 250 break; 251 252 case EliminarDisco: 253 printf ("T´ıtulo: "); gets(titulo_disco); 254 printf ("Int´erprete: "); gets(interprete); 255 coleccion = borra_disco_por_titulo_e_interprete(coleccion, titulo_disco, interprete); 256 break; 257 } 258 } while (opcion != Salir); 259 260 coleccion = libera_coleccion(coleccion); 261 262 return 0; 263 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 307 Modifica el programa para que se almacene la duraci´on de cada canci´on (en segundos) junto al t´ıtulo de la misma. Introducci´on a la Programaci´on con C 317
  • 324. 4.12 Otras estructuras de datos con registros enlazados · 308 La funci´on de b´usqueda de discos por int´erprete se detiene al encontrar el primer disco de un int´erprete dado. Modifica la funci´on para que devuelva una lista con una copia de todos los discos de un int´erprete. Usa esa lista para mostrar su contenido por pantalla con muestra_coleccion y elim´ınala una vez hayas mostrado su contenido. · 309 Dise˜na una aplicaci´on para la gesti´on de libros de una biblioteca. Debes mantener dos listas: una lista de libros y otra de socios. De cada socio recordamos el nombre, el DNI y el tel´efono. De cada libro mantenemos los siguientes datos: t´ıtulo, autor, ISBN, c´odigo de la biblioteca (una cadena con 10 caracteres) y estado. El estado es un puntero que, cuando vale NULL, indica que el libro est´a disponible y, en caso contrario, apunta al socio al que se ha prestado el libro. El programa debe permitir dar de alta y baja libros y socios, as´ı como efectuar el pr´estamo de un libro a un socio y gestionar su devoluci´on. Ten en cuenta que no es posible dar de baja a un socio que posee un libro en pr´estamo ni dar de baja un libro prestado. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.12. Otras estructuras de datos con registros enlazados La posibilidad de trabajar con registros enlazados abre las puertas al dise˜no de estructuras de datos muy elaboradas que permiten efectuar ciertas operaciones muy eficientemente. El precio a pagar es una mayor complejidad de nuestros programas C y, posiblemente, un mayor consumo de memoria (estamos almacenando valores y punteros, aunque s´olo nos interesan los valores). Pero no has visto m´as que el principio. En otras asignaturas de la carrera aprender´as a utilizar estructuras de datos complejas, pero capaces de ofrecer tiempos de respuesta mucho mejores que las listas que hemos estudiado o capaces de permitir implementaciones sencillas para operaciones que a´un no hemos estudiado. Te vamos a presentar unos pocos ejemplos ilustrativos. Las listas circulares, por ejemplo, son listas sin final. El nodo siguiente al que parece el ´ultimo nodo es el primero. Ning´un nodo est´a ligado a NULL. lista 3 info sig 8 info sig 2 info sig Este tipo de estructura de datos es ´util, por ejemplo, para mantener una lista de tareas a las que hay que ir dedicando atenci´on rotativamente: cuando hemos hecho una ronda, queremos pasar nuevamente al primer elemento. El campo sig del ´ultimo elemento permite pasar directamente al primero, con lo que resulta sencillo codificar un bucle que recorre rotativamente la lista. En muchas aplicaciones es preciso trabajar con matrices dispersas. Una matriz dispersa es una matriz en la que muy pocos componentes presentan un valor diferente de cero. Esta matriz, por ejemplo, es dispersa:                 0 0 2.5 0 0 1.2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3.7 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1.3 8.1 0 0 0 0 0.2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0                 De los 100 componentes de esta matriz de 10 × 10, tan s´olo hay 6 no nulos. Las matri- ces dispersas pueden representarse con listas de listas para ahorrar memoria. Una lista mantiene las filas que, a su vez, son listas de valores no nulos. En estas ´ultimas listas, cada nodo almacena la columna del valor no nulo y el propio valor. La matriz dispersa del ejemplo se representar´ıa as´ı (suponiendo que filas y columnas empiezan numer´andose en 1, como es habitual en matem´aticas): 318 Introducci´on a la Programaci´on con C
  • 325. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 4 Estructuras de datos: memoria din´amica matriz 1 sig fila cols 3 2.5 columna valor sig 6 1.2 columna valor sig 3 sig fila cols 2 3.7 columna valor sig 6 sig fila cols 2 1.3 columna valor sig 3 8.1 columna valor sig 8 0.2 columna valor sig El ahorro de memoria es notabil´ısimo: si un float ocupa 8 bytes, hemos pasado de 800 a 132 bytes consumidos. El ahorro es relativamente mayor cuanto mayor es la matriz. Eso s´ı, la complejidad de los algoritmos que manipulan esa estructura es tambi´en notabil´ısima. ¡Imagina el procedimiento que permite multiplicar eficientemente dos matrices dispersas representadas as´ı! Un ´arbol binario de b´usqueda es una estructura montada con registros enlazados, pero no es una lista. Cada nodo tiene cero, uno o dos hijos: uno a su izquierda y uno a su derecha. Los nodos que no tienen hijos se llaman hojas. El nodo m´as alto, del que descienden todos los dem´as, se llama nodo ra´ız. Los descendientes de un nodo (sus hijos, nietos, biznietos, etc.) tienen una curiosa propiedad: si descienden por su izquierda, tienen valores m´as peque˜nos que el de cualquier ancestro, y si descienden por su derecha, valores mayores. Aqu´ı tienes un ejemplo de ´arbol binario de b´usqueda: raiz 10 der info izq 3 der info izq 15 der info izq 1 der info izq 6 der info izq 12 der info izq 23 der info izq Una ventaja de los ´arboles binarios de b´usqueda es la rapidez con que pueden resolver la pregunta ((¿pertenece un valor determinado al conjunto de valores del ´arbol?)). Hay un m´etodo recursivo que recibe un puntero a un nodo y dice: • si el puntero vale NULL; la respuesta es no; • si el valor coincide con el del nodo apuntado, la respuesta es s´ı; • si el valor es menor que el valor del nodo apuntado, entonces la respuesta la conoce el hijo izquierdo, por lo que se le pregunta a ´el (recursivamente); • y si el valor es mayor que el valor del nodo apuntado, entonces la respuesta la conoce el hijo derecho, por lo que se le pregunta a ´el (recursivamente). Ingenioso, ¿no? Observa que muy pocos nodos participan en el c´alculo de la respuesta. Si deseas saber, por ejemplo, si el 6 pertenece al ´arbol de la figura, s´olo hay que preguntarle a los nodos que tienen el 10, el 3 y el 6. El resto de nodos no se consultan para nada. Siempre es posible responder a una pregunta de pertenencia en un ´arbol con n nodos visitando un n´umero de nodos que es, a lo sumo, igual a 1 + log2 n. Rapid´ısimo. ¿Qu´e costar´a, a cambio, insertar o borrar un nodo en el ´arbol? Cabe pensar que mucho m´as que un tiempo proporcional al n´umero de nodos, pues la estructura de los enlaces es muy compleja. Pero no es as´ı. Existen procedimientos sofisticados que consiguen efectuar esas operaciones en tiempo proporcional ¡al logaritmo en base 2 del n´umero de nodos! Introducci´on a la Programaci´on con C 319
  • 326. 4.12 Otras estructuras de datos con registros enlazados Hay muchas m´as estructuras de datos que permiten acelerar sobremanera los programas que gestionan grandes conjuntos de datos. Apenas hemos empezado a conocer y aprendido a manejar las herramientas con las que se construyen los programas: las estructuras de datos y los algoritmos. 320 Introducci´on a la Programaci´on con C
  • 327. Cap´ıtulo 5 Ficheros —Me temo que s´ı, se˜nora —dijo Alicia—. No recuerdo las cosas como sol´ıa. . . ¡y no conservo el mismo tama˜no diez minutos seguidos! Lewis Carroll, Alicia en el Pa´ıs de las Maravillas. Acabamos nuestra introducci´on al lenguaje C con el mismo objeto de estudio con el que finaliza- mos la presentaci´on del lenguaje Python: los ficheros. Los ficheros permiten guardar informaci´on en un dispositivo de almacenamiento de modo que ´esta ((sobreviva)) a la ejecuci´on de un pro- grama. No te vendr´ıa mal repasar los conceptos introductorios a ficheros antes de empezar. 5.1. Ficheros de texto y ficheros binarios Con Python estudiamos ´unicamente ficheros de texto. Con C estudiaremos dos tipos de ficheros: ficheros de texto y ficheros binarios. 5.1.1. Representaci´on de la informaci´on en los ficheros de texto Ya conoces los ficheros de texto: contienen datos legibles por una persona y puedes generarlos o modificarlos desde tus propios programas o usando aplicaciones como los editores de texto. Los ficheros binarios, por contra, no est´an pensados para facilitar su lectura por parte de seres humanos (al menos no directamente). Pongamos que se desea guardar un valor de tipo entero en un fichero de texto, por ejemplo, el valor 12. En el fichero de texto se almacenar´a el d´ıgito ’1’ (codificado en ASCII como el valor 49) y el d´ıgito ’2’ (codificado en ASCII como el valor 50), es decir, dos datos de tipo char. A la hora de leer el dato, podremos leerlo en cualquier variable de tipo entero con capacidad suficiente para almacenar ese valor (un char, un unsigned char, un int, un unsigned int, etc.). Esto es as´ı porque la lectura de ese dato pasa por un proceso de interpretaci´on relativamente sofisticado: cuando se lee el car´acter ’1’, se memoriza el valor 1; y cuando se lee el car´acter ’2’, se multiplica por 10 el valor memorizado y se le suma el valor 2. As´ı se llega al valor 12, que es lo que se almacena en la variable en cuesti´on. Observa que, codificado como texto, 12 ocupa dos bytes, pero que si se almacena en una variable de tipo char ocupa 1 y en una variable de tipo int ocupa 4. Un problema de los ficheros de texto es la necesidad de usar marcas de separaci´on entre sus diferentes elementos. Si, por ejemplo, al valor 12 ha de sucederle el valor 100, no podemos limitarnos a disponer uno a continuaci´on del otro sin m´as, pues el fichero contendr´ıa la siguiente secuencia de caracteres: 1 2 1 0 0 ¿Qu´e estamos representando exactamente? ¿Un 12 seguido de un 100 o un 1 seguido de un 2100? ¿Y por qu´e no un 1210 seguido de un 0 o, sencillamente, el valor 12100, sin m´as? Las marcas de separaci´on son caracteres que decide el programador, pero es corriente que se trate de espacios en blanco, tabuladores o saltos de l´ınea. El valor 12 seguido del valor 100 podr´ıa representarse, pues, con cualquiera de estas secuencias de caracteres: Introducci´on a la Programaci´on con C 321
  • 328. 5.1 Ficheros de texto y ficheros binarios 1 2 1 0 0 1 2 t 1 0 0 1 2 n 1 0 0 Usar caracteres separadores es fuente, naturalmente, de un coste adicional: un mayor tama˜no de los ficheros. Cuando los separadores son espacios en blanco, es frecuente permitir libertad en cuanto a su n´umero: 1 2 n 1 0 0 n Las herramientas con las que leemos los datos de ficheros de texto saben lidiar con las compli- caciones que introducen estos separadores blancos repetidos. Los ficheros de texto cuentan con la ventaja de que se pueden inspeccionar con ayuda de un editor de texto y permiten as´ı, por lo general, deducir el tipo de los diferentes datos que lo componen, pues ´estos resultan legibles. 5.1.2. Representaci´on de la informaci´on en los ficheros binarios Los ficheros binarios requieren una mayor precisi´on en la determinaci´on de la codificaci´on de la informaci´on. Si almacenamos el valor 12 en un fichero binario, hemos de decidir si queremos almacenarlo como car´acter con o sin signo, como entero con o sin signo, etc. La decisi´on adoptada determinar´a la ocupaci´on de la informaci´on (uno o cuatro bytes) y su codificaci´on (binario natural o complemento a dos). Si guardamos el 12 como un char, guardaremos un solo byte formado por estos 8 bits: 00001100 Pero si optamos por almacenarlo como un int, ser´an cuatro los bytes escritos: 00000000 00000000 00000000 00001100 Un mismo patr´on de 8 bits, como 11111111 tiene dos interpretaciones posibles: el valor 255 si entendemos que es un dato de tipo unsig- ned char o el valor −1 si consideramos que codifica un dato de tipo char.1 Como puedes ver, la secuencia de bits que escribimos en el fichero es exactamente la misma que hay almacenada en la memoria, usando la mism´ısima codificaci´on binaria. De ah´ı el nombre de ficheros binarios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 310 ¿Qu´e ocupa en un fichero de texto cada uno de estos datos? a) 1 b) 0 c) 12 d) -15 e) 128 f) 32767 g) -32768 h) 2147483647 i) -2147483648 ¿Y cu´anto ocupa cada uno de ellos si los almacenamos en un fichero binario como valores de tipo int? 1Un fichero de texto no presentar´ıa esta ambig¨uedad: el n´umero se habr´ıa escrito como −1 o como 255. S´ı que presentar´ıa, sin embargo, un punto de elecci´on reservado al programador: aunque −1 lleva signo y por tanto se almacenar´a en una variable de alg´un tipo con signo, ¿queremos almacenarlo en una variable de tipo char, una variable de tipo int o, por qu´e no, en una variable de tipo float? 322 Introducci´on a la Programaci´on con C
  • 329. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros · 311 ¿C´omo se interpreta esta secuencia de bytes en cada uno de los siguientes supuestos? 00000000 00000000 00000000 00001100 a) Como cuatro datos de tipo char. b) Como cuatro datos de tipo unsigned char. c) Como un dato de tipo int. d) Como un dato de tipo unsigned int. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Escribir dos o m´as datos de un mismo tipo en un fichero binario no requiere la inserci´on de marcas separadoras: cada cierto n´umero de bytes empieza un nuevo dato (cada cuatro bytes, por ejemplo, empieza un nuevo int), as´ı que es f´acil decidir d´onde empieza y acaba cada dato. La lectura de un fichero binario requiere un conocimiento exacto del tipo de datos de cada uno de los valores almacenados en ´el, pues de lo contrario la secuencia de bits carecer´a de un significado definido. Los ficheros binarios no s´olo pueden almacenar escalares. Puedes almacenar tambi´en regis- tros y vectores pues, a fin de cuentas, no son m´as que patrones de bits de tama˜no conocido. Lo ´unico que no debe almacenarse en ficheros binarios son los punteros. La raz´on es sencilla: si un puntero apunta a una zona de memoria reservada con malloc, su valor es la direcci´on del primer byte de esa zona. Si guardamos ese valor en disco y lo recuperamos m´as tarde (en una ejecuci´on posterior, por ejemplo), esa zona puede que no haya sido reservada. Acceder a ella provocar´a, en consecuencia, un error capaz de abortar la ejecuci´on del programa. Por regla general, los ficheros binarios son m´as compactos que los ficheros de texto, pues cada valor ocupa lo mismo que ocupar´ıa en memoria. La lectura (y escritura) de los datos de ficheros binarios es tambi´en m´as r´apida, ya que nos ahorramos el proceso de conversi´on del formato de texto al de representaci´on de informaci´on en memoria y viceversa. Pero no todo son ventajas. Portabilidad de ficheros Los ficheros binarios presentan algunos problemas de portabilidad, pues no todos los or- denadores almacenan en memoria los valores num´ericos de la misma forma: los ficheros binarios escritos en un ordenador ((big-endian)) no son directamente legibles en un ordenador ((little-endian)). Los ficheros de texto son, en principio, m´as portables, pues la tabla ASCII es un est´andar ampliamente aceptado para el intercambio de ficheros de texto. No obstante, la tabla ASCII es un c´odigo de 7 bits que s´olo da cobertura a los s´ımbolos propios de la escritura del ingl´es y algunos caracteres especiales. Los caracteres acentuados, por ejemplo, est´an excluidos. En los ´ultimos a˜nos se ha intentado implantar una familia de est´andares que den cobertura a estos y otros caracteres. Como 8 bits resultan insuficientes para codificar todos los caracteres usados en la escritura de cualquier lenguaje, hay diferentes subconjuntos para cada una de las diferentes comunidades culturales. Las lenguas rom´anicas occidentales usan el est´andar IsoLatin-1 (o ISO-8859-1), recientemente ampliado con el s´ımbolo del euro para dar lugar al IsoLatin-15 (o ISO-8859-15). Los problemas de portabilidad surgen cuando interpretamos un fichero de texto codificado con IsoLatin-1 como si estuviera codificado con otro est´andar: no veremos m´as que un galimat´ıas de s´ımbolos extra˜nos all´ı donde se usan caracteres no ASCII. 5.2. Ficheros de texto 5.2.1. Abrir, leer/escribir, cerrar Los ficheros de texto se manipulan en C siguiendo el mismo ((protocolo)) que segu´ıamos en Python: 1. Se abre el fichero en modo lectura, escritura, adici´on, o cualquier otro modo v´alido. Introducci´on a la Programaci´on con C 323
  • 330. 5.2 Ficheros de texto 2. Se trabaja con ´el leyendo o escribiendo datos, seg´un el modo de apertura escogido. Al abrir un fichero se dispone un ((cabezal)) de lectura o escritura en un punto definido del fichero (el principio o el final). Cada acci´on de lectura o escritura desplaza el cabezal de izquierda a derecha, es decir, de principio a final del fichero. 3. Se cierra el fichero. Bueno, lo cierto es que, como siempre en C, hay un paso adicional y previo a estos tres: la declaraci´on de una variable de ((tipo fichero)). La cabecera stdio.h incluye la definici´on de un tipo de datos llamado FILE y declara los prototipos de las funciones de manipulaci´on de ficheros. Nuestra variable de tipo fichero ha de ser un puntero a FILE, es decir, ha de ser de tipo FILE *. Las funciones b´asicas con las que vamos a trabajar son: fopen: abre un fichero. Recibe la ruta de un fichero (una cadena) y el modo de apertura (otra cadena) y devuelve un objeto de tipo FILE *. FILE * fopen (char ruta[], char modo[]); Los modos de apertura para ficheros de texto con los que trabajaremos son ´estos: • "r" (lectura): El primer car´acter le´ıdo es el primero del fichero. • "w" (escritura): Trunca el fichero a longitud 0. Si el fichero no existe, se crea. • "a" (adici´on): Es un modo de escritura que preserva el contenido original del fichero. Los caracteres escritos se a˜naden al final del fichero. Si el fichero no puede abrirse por cualquier raz´on, fopen devuelve el valor NULL. (Observa que los modos se indican con cadenas, no con caracteres: debes usar comillas dobles.) Modos de apertura para lectura y escritura simult´anea Los modos "r", "w" y "a" no son los ´unicos v´alidos para los ficheros de texto. Puedes usar, adem´as, ´estos otros: "r+", "w+" y "a+". Todos ellos abren los ficheros en modo de lectura y escritura a la vez. Hay, no obstante, matices que los diferencian: • "r+": No se borra el contenido del fichero, que debe existir previamente. El ((cabezal)) de lectura/escritura se sit´ua al principio del fichero. • "w+": Si el fichero no existe, se crea, y si existe, se trunca el contenido a longitud cero. El ((cabezal)) de lectura/escritura se sit´ua al principio del fichero. • "a+": Si el fichero no existe, se crea. El ((cabezal)) de lectura/escritura se sit´ua al final del fichero. Una cosa es que existan estos m´etodos y otra que te recomendemos su uso. Te lo desaconsejamos. Resulta muy dif´ıcil escribir en medio de un fichero de texto a voluntad sin destruir la informaci´on previamente existente en ´el, pues cada l´ınea puede ocupar un n´umero de caracteres diferente. fclose: cierra un fichero. Recibe el FILE * devuelto por una llamada previa a fopen. int fclose (FILE * fichero); El valor devuelto por fclose es un c´odigo de error que nos advierte de si hubo un fallo al cerrar el fichero. El valor 0 indica ´exito y el valor EOF (predefinido en stdio.h) indica error. M´as adelante indicaremos c´omo obtener informaci´on adicional acerca del error detectado. Cada apertura de un fichero con fopen debe ir acompa˜nada de una llamada a fclose una vez se ha terminado de trabajar con el fichero. fscanf : lee de un fichero. Recibe un fichero abierto con fopen (un FILE *), una cadena de formato (usando las marcas de formato que ya conoces por scanf ) y las direcciones de memoria en las que debe depositar los valores le´ıdos. La funci´on devuelve el n´umero de elementos efectivamente le´ıdos (valor que puedes usar para comprobar si la lectura se complet´o con ´exito). 324 Introducci´on a la Programaci´on con C
  • 331. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros int fscanf (FILE * fichero, char formato[], direcciones ); fprintf : escribe en un fichero. Recibe un fichero abierto con fopen (un FILE *), una cade- na de formato (donde puedes usar las marcas de formato que aprendiste a usar con printf ) y los valores que deseamos escribir. La funci´on devuelve el n´umero de caracteres efectiva- mente escritos (valor que puedes usar para comprobar si se escribieron correctamente los datos). int fprintf (FILE * fichero, char formato[], valores ); feof : devuelve 1 si estamos al final del fichero y 0 en caso contrario. El nombre de la funci´on es abreviatura de ((end of file)) (en espa˜nol, ((fin de fichero))). ¡Ojo! S´olo tiene sentido consultar si se est´a o no al final de fichero tras efectuar una lectura de datos. (Este detalle complicar´a un poco las cosas.) int feof (FILE * fichero); Como puedes ver no va a resultar muy dif´ıcil trabajar con ficheros de texto en C. A fin de cuentas, las funciones de escritura y lectura son b´asicamente id´enticas a printf y scanf , y ya hemos aprendido a usarlas. La ´unica novedad destacable es la nueva forma de detectar si hemos llegado al final de un fichero o no: ya no se devuelve la cadena vac´ıa como consecuencia de una lectura al final del fichero, como ocurr´ıa en Python, sino que hemos de preguntar expl´ıcitamente por esa circunstancia usando una funci´on (feof ). Nada mejor que un ejemplo para aprender a utilizar ficheros de texto en C. Vamos a generar los 1000 primeros n´umeros primos y a guardarlos en un fichero de texto. Cada n´umero se escribir´a en una l´ınea. genera primos.c genera primos.c 1 #include <stdio.h> 2 3 int es_primo(int n) 4 { 5 int i, j, primo; 6 primo = 1; 7 for (j=2; j<=n/2; j++) 8 if (n % j == 0) { 9 primo = 0; 10 break; 11 } 12 return primo; 13 } 14 15 int main(void) 16 { 17 FILE * fp; 18 int i, n; 19 20 fp = fopen("primos.txt", "w"); 21 i = 1; 22 n = 0; 23 while (n<1000) { 24 if (es_primo(i)) { 25 fprintf (fp, "%dn", i); 26 n++; 27 } 28 i++; 29 } 30 fclose(fp); 31 32 return 0; 33 } Introducci´on a la Programaci´on con C 325
  • 332. 5.2 Ficheros de texto Hemos llamado a la variable de fichero fp por ser abreviatura del t´ermino ((file pointer)) (puntero a fichero). Es frecuente utilizar ese nombre para las variables de tipo FILE *. Una vez compilado y ejecutado el programa genera primos obtenemos un fichero de texto llamado primos.txt del que te mostramos sus primeras y ´ultimas l´ıneas (puedes comprobar la correcci´on del programa abriendo el fichero primos.txt con un editor de texto): primos.txt 1 1 2 2 3 3 4 5 5 7 6 11 7 13 8 17 9 19 10 23 ... 990 7823 991 7829 992 7841 993 7853 994 7867 995 7873 996 7877 997 7879 998 7883 999 7901 1000 7907 Aunque en pantalla lo vemos como una secuencia de l´ıneas, no es m´as que una secuencia de caracteres: 1 n 2 n 3 n 5 n . . . 7 9 0 1 n 7 9 0 7 n Dise˜nemos ahora un programa que lea el fichero primos.txt generado por el programa anterior y muestre por pantalla su contenido: 1 #include <stdio.h> 2 3 int main(void) 4 { 5 FILE * fp; 6 int i; 7 8 fp = fopen("primos.txt", "r"); 9 fscanf (fp, "%d", &i); 10 while ( !feof (fp) ) { 11 printf ("%dn", i); 12 fscanf (fp, "%d", &i); 13 } 14 fclose(fp); 15 16 return 0; 17 } Observa que la llamada a fscanf se encuentra en un bucle que se lee as´ı ((mientras no se haya acabado el fichero. . . )), pues feof averigua si hemos llegado al final del fichero. La l´ınea 9 contiene una lectura de datos para que la consulta a feof tenga sentido: feof s´olo actualiza su valor tras efectuar una operaci´on de lectura del fichero. Si no te gusta la aparici´on de dos sentencias fscanf , puedes optar por esta alternativa: 326 Introducci´on a la Programaci´on con C
  • 333. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros 1 #include <stdio.h> 2 3 int main(void) 4 { 5 FILE * fp; 6 int i; 7 8 fp = fopen("primos.txt", "r"); 9 while (1) { 10 fscanf (fp, "%d", &i); 11 if (feof (fp)) break; 12 printf ("%dn", i); 13 } 14 fclose(fp); 15 16 return 0; 17 } Y si deseas evitar el uso de break, considera esta otra: lee primos.c lee primos.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 FILE * fp; 6 int i; 7 8 fp = fopen("primos.txt", "r"); 9 do { 10 fscanf (fp, "%d", &i); 11 if (!feof (fp)) 12 printf ("%dn", i); 13 } while (!feof (fp)); 14 fclose(fp); 15 16 return 0; 17 } ¿Y si el fichero no existe? Al abrir un fichero puede que detectes un error: fopen devuelve la direcci´on NULL. Hay varias razones, pero una que te ocurrir´a al probar algunos de los programas del texto es que el fichero que se pretende leer no existe. Una soluci´on puede consistir en crearlo en ese mismo instante: 1 f = fopen(ruta, "r"); 2 if (f == NULL) { 3 f = fopen(ruta, "w"); 4 fclose(f); 5 f = fopen(ruta, "r"); 6 } Si el problema era la inexistencia del fichero, este truco funcionar´a, pues el modo "w" lo crea cuando no existe. Es posible, no obstante, que incluso este m´etodo falle. En tal caso, es probable que tengas un problema de permisos: ¿tienes permiso para leer ese fichero?, ¿tienes permiso para escribir en el directorio en el que reside o debe residir el fichero? M´as adelante prestaremos atenci´on a esta cuesti´on. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 312 Dise˜na un programa que a˜nada al fichero primos.txt los 100 siguientes n´umeros pri- mos. El programa leer´a el contenido actual del fichero para averiguar cu´al es el ´ultimo primo del Introducci´on a la Programaci´on con C 327
  • 334. 5.2 Ficheros de texto fichero. A continuaci´on, abrir´a el fichero en modo adici´on ("a") y a˜nadir´a 100 nuevos primos. Si ejecut´asemos una vez genera primos y, a continuaci´on, dos veces el nuevo programa, el fichero acabar´ıa conteniendo los 1200 primeros primos. · 313 Dise˜na un programa que lea de teclado una frase y escriba un fichero de texto llamado palabras.txt en el que cada palabra de la frase ocupa una l´ınea. · 314 Dise˜na un programa que lea de teclado una frase y escriba un fichero de texto llamado letras.txt en el que cada l´ınea contenga un car´acter de la frase. · 315 Modifica el programa miniGalaxis para que gestione una lista de records. Un fichero de texto, llamado minigalaxis.records almacenar´a el nombre y n´umero de movimientos de los 5 mejores jugadores de todos los tiempos (los que completaron el juego usando el menor n´umero de sondas). · 316 Disponemos de dos ficheros: uno contiene un diccionario y el otro, un texto. El diccio- nario est´a ordenado alfab´eticamente y contiene una palabra en cada l´ınea. Dise˜na un programa que lea el diccionario en un vector de cadenas y lo utilice para detectar errores en el texto. El programa mostrar´a por pantalla las palabras del texto que no est´an en el diccionario, indicando los n´umeros de l´ınea en que aparecen. Supondremos que el diccionario contiene, a lo sumo, 1000 palabras y que la palabra m´as larga (tanto en el diccionario como en el texto) ocupa 30 caracteres. (Si quieres usar un diccionario real como el descrito y trabajas en Unix, encontrar´as uno en ingl´es en /usr/share/dict/words o /usr/dict/words. Puedes averiguar el n´umero de palabras que contiene con el comando wc de Unix.) · 317 Modifica el programa del ejercicio anterior para que el n´umero de palabras del vector que las almacena se ajuste autom´aticamente al tama˜no del diccionario. Tendr´as que usar memoria din´amica. Si usas un vector de palabras, puedes efectuar dos pasadas de lectura en el fichero que contiene el diccionario: una para contar el n´umero de palabras y saber as´ı cu´anta memoria es necesaria y otra para cargar la lista de palabras en un vector din´amico. Naturalmente, antes de la segunda lectura deber´as haber reservado la memoria necesaria. Una alternativa a leer dos veces el fichero consiste en usar realloc juiciosamente: reserva inicialmente espacio para, digamos, 1000 palabras; si el diccionario contiene un n´umero de palabras mayor que el que cabe en el espacio de memoria reservada, duplica la capacidad del vector de palabras (cuantas veces sea preciso si el problema se da m´as de una vez). Otra posibilidad es usar una lista simplemente enlazada, pues puedes crearla con una primera lectura. Sin embargo, no es recomendable que sigas esta estrategia, pues no podr´as efectuar una b´usqueda dicot´omica a la hora de determinar si una palabra est´a incluida o no en el diccionario. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ya vimos en su momento que fscanf presenta un problema cuando leemos cadenas: s´olo lee una ((palabra)), es decir, se detiene al llegar a un blanco. Aprendimos a usar entonces una funci´on, gets, que le´ıa una l´ınea completa. Hay una funci´on equivalente para ficheros de texto: char * fgets(char cadena[], int max_tam, FILE * fichero ); ¡Ojo con el prototipo de fgets! ¡El par´ametro de tipo FILE * es el ´ultimo, no el primero! Otra incoherencia de C. El primer par´ametro es la cadena en la que se desea depositar el resultado de la lectura. El segundo par´ametro, un entero, es una medida de seguridad: es el m´aximo n´umero de bytes que queremos leer en la cadena. Ese l´ımite permite evitar peligrosos desbordamientos de la zona de memoria reservada para cadena cuando la cadena le´ıda es m´as larga de lo previsto. El ´ultimo par´ametro es, finalmente, el fichero del que vamos a leer (previamente se ha abierto con fopen). La funci´on se ocupa de terminar correctamente la cadena le´ıda con un ’0’, pero respetando el salto de l´ınea (n) si lo hubiera.2 En caso de querer suprimir el retorno de l´ınea, puedes invocar una funci´on como ´esta sobre la cadena le´ıda: 1 void quita_fin_de_linea(char linea[]) 2 { 3 int i; 4 for (i=0; linea[i] != ’0’; i++) 5 if (linea[i] == ’n’) { 6 linea[i] = ’0’; 2En esto se diferencia de gets. 328 Introducci´on a la Programaci´on con C
  • 335. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros 7 break; 8 } 9 } La funci´on fgets devuelve una cadena (un char *). En realidad, es un puntero a la propia variable cadena cuando todo va bien, y NULL cuando no se ha podido efectuar la lectura. El valor de retorno es ´util, ´unicamente, para hacer detectar posibles errores tras llamar a la funci´on. Hay m´as funciones de la familia get. La funci´on fgetc, por ejemplo, lee un car´acter: int fgetc(FILE * fichero); No te equivoques: devuelve un valor de tipo int, pero es el valor ASCII de un car´acter. Puedes asignar ese valor a un unsigned char, excepto cuando vale EOF (de ((end of file))), que es una constante (cuyo valor es −1) que indica que no se pudo leer el car´acter requerido porque llegamos al final del fichero. Las funciones fgets y fgetc se complementan con fputs y fputc, que en lugar de leer una cadena o un car´acter, escriben una cadena o un car´acter en un fichero abierto para escritura o adici´on. He aqu´ı sus prototipos: int fputs(char cadena[], FILE * fichero); int fputc(int caracter, FILE * fichero); Al escribir una cadena con fputs, el terminador ’0’ no se escribe en el fichero. Pero no te preocupes: fgets ((lo sabe)) y lo introduce autom´aticamente en el vector de caracteres al leer del fichero. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 318 Hemos escrito este programa para probar nuestra comprensi´on de fgets y fputs (presta atenci´on tambi´en a los blancos, que se muestran con el car´acter ): 1 #include <stdio.h> 2 #include <string.h> 3 4 #define MAXLON 100 5 6 int main (void) 7 { 8 FILE * f; 9 char s[MAXLON+1]; 10 char * aux; 11 12 f = fopen("prueba.txt", "w"); 13 fputs("si", f); 14 fputs("non", f); 15 fclose(f); 16 17 f = fopen("prueba.txt", "r"); 18 aux = fgets(s, MAXLON, f); 19 printf ("%s %sn", aux, s); 20 aux = fgets(s, MAXLON, f); 21 printf ("%s %sn", aux, s); 22 fclose(f); 23 24 return 0; 25 } Primera cuesti´on: ¿Cu´antos bytes ocupa el fichero prueba.txt? Al ejecutarlo, obtenemos este resultado en pantalla: sino sino (null) sino Segunda cuesti´on: ¿Puedes explicar con detalle qu´e ha ocurrido? (El texto (((null))) es escrito autom´aticamente por printf cuando se le pasa como cadena un puntero a NULL.) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Introducci´on a la Programaci´on con C 329
  • 336. 5.2 Ficheros de texto 5.2.2. Aplicaciones: una agenda y un gestor de una colecci´on de discos compactos Lo aprendido nos permite ya dise˜nar programas capaces de escribir y leer colecciones de datos en ficheros de texto. Una agenda Vamos a desarrollar un peque˜no ejemplo centrado en las rutinas de entrada/salida para la ges- ti´on de una agenda montada con una lista simplemente enlazada. En la agenda, que cargaremos de un fichero de texto, tenemos el nombre, la direcci´on y el tel´efono de varias personas. Cada entrada en la agenda se representar´a con tres l´ıneas del fichero de texto. He aqu´ı un ejemplo de fichero con este formato: agenda.txt agenda.txt 1 Juan Gil 2 Ronda Mijares, 1220 3 964 123456 4 Ana Garc´ıa 5 Plaza del Sol, 13 6 964-872777 7 Pepe P´erez 8 Calle de Arriba, 1 9 964 263 263 Nuestro programa podr´a leer en memoria los datos de un fichero como ´este y tambi´en escribirlos en fichero desde memoria. Las estructuras de datos que manejaremos en memoria se definen as´ı: 1 struct Entrada { 2 char * nombre; 3 char * direccion; 4 char * telefono; 5 }; 6 7 struct NodoAgenda { 8 struct Entrada datos; 9 struct NodoAgenda * sig; 10 }; 11 12 typedef struct NodoAgenda * TipoAgenda; Al final del apartado presentamos el programa completo. Centr´emonos ahora en las funciones de escritura y lectura del fichero. La rutina de escritura de datos en un fichero recibir´a la estructura y el nombre del fichero en el que guardamos la informaci´on. Guardaremos cada entrada de la agenda en tres l´ıneas: una por cada campo. 1 void escribe_agenda(TipoAgenda agenda, char nombre_fichero[]) 2 { 3 struct NodoAgenda * aux; 4 FILE * fp; 5 6 fp = fopen(nombre_fichero, "w"); 7 for (aux=agenda; aux!=NULL; aux=aux->sig) 8 fprintf (fp, "%sn%sn%sn", aux->datos.nombre, 9 aux->datos.direccion, 10 aux->datos.telefono); 11 fclose(fp); 12 } La lectura del fichero ser´a sencilla: 1 TipoAgenda lee_agenda(char nombre_fichero[]) 2 { 330 Introducci´on a la Programaci´on con C
  • 337. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros 3 TipoAgenda agenda; 4 struct Entrada * entrada_leida; 5 FILE * fp; 6 char nombre[MAXCADENA+1], direccion[MAXCADENA+1], telefono[MAXCADENA+1]; 7 int longitud; 8 9 agenda = crea_agenda(); 10 11 fp = fopen(nombre_fichero, "r"); 12 while (1) { 13 fgets(nombre, MAXCADENA, fp); 14 if (feof (fp)) break; // Si se acab´o el fichero, acabar la lectura. 15 quita_fin_de_linea(nombre); 16 17 fgets(direccion, MAXCADENA, fp); 18 quita_fin_de_linea(direccion); 19 20 fgets(telefono, MAXCADENA, fp); 21 quita_fin_de_linea(telefono); 22 23 agenda = anyadir_entrada(agenda, nombre, direccion, telefono); 24 } 25 fclose(fp); 26 27 return agenda; 28 } La ´unica cuesti´on rese˜nable es la purga de saltos de l´ınea innecesarios. He aqu´ı el listado completo del programa: agenda sencilla.c agenda sencilla.c 1 #include <stdio.h> 2 #include <stdlib.h> 3 4 #define MAXCADENA 200 5 6 enum { Ver=1, Alta, Buscar, Salir }; 7 8 struct Entrada { 9 char * nombre; 10 char * direccion; 11 char * telefono; 12 }; 13 14 struct NodoAgenda { 15 struct Entrada datos; 16 struct NodoAgenda * sig; 17 }; 18 19 typedef struct NodoAgenda * TipoAgenda; 20 21 void quita_fin_de_linea(char linea[]) 22 { 23 int i; 24 for (i=0; linea[i] != ’0’; i++) 25 if (linea[i] == ’n’) { 26 linea[i] = ’0’; 27 break; 28 } 29 } 30 31 void muestra_entrada(struct NodoAgenda * e) 32 // Podr´ıamos haber pasado e por valor, pero resulta m´as eficiente (y no mucho m´as 33 // inc´omodo) hacerlo por referencia: pasamos as´ı s´olo 4 bytes en lugar de 12. Introducci´on a la Programaci´on con C 331
  • 338. 5.2 Ficheros de texto 34 { 35 printf ("Nombre : %sn", e->datos.nombre); 36 printf ("Direcci´on: %sn", e->datos.direccion); 37 printf ("Tel´efono : %sn", e->datos.telefono); 38 } 39 40 void libera_entrada(struct NodoAgenda * e) 41 { 42 int i; 43 44 free(e->datos.nombre); 45 free(e->datos.direccion); 46 free(e->datos.telefono); 47 free(e); 48 } 49 50 51 TipoAgenda crea_agenda(void) 52 { 53 return NULL; 54 } 55 56 TipoAgenda anyadir_entrada(TipoAgenda agenda, char nombre[], 57 char direccion[], char telefono[]) 58 { 59 struct NodoAgenda * aux, * e; 60 61 /* Averiguar si ya tenemos una persona con ese nombre */ 62 if (buscar_entrada_por_nombre(agenda, nombre) != NULL) 63 return agenda; 64 65 /* Si llegamos aqu´ı, es porque no ten´ıamos registrada a esa persona. */ 66 e = malloc(sizeof(struct NodoAgenda)); 67 e->datos.nombre = malloc((strlen(nombre)+1)*sizeof(char)); 68 strcpy(e->datos.nombre, nombre); 69 e->datos.direccion = malloc((strlen(direccion)+1)*sizeof(char)); 70 strcpy(e->datos.direccion, direccion); 71 e->datos.telefono = malloc((strlen(telefono)+1)*sizeof(char)); 72 strcpy(e->datos.telefono, telefono); 73 e->sig = agenda; 74 agenda = e; 75 return agenda; 76 } 77 78 void muestra_agenda(TipoAgenda agenda) 79 { 80 struct NodoAgenda * aux; 81 82 for (aux = agenda; aux != NULL; aux = aux->sig) 83 muestra_entrada(aux); 84 } 85 86 struct NodoAgenda * buscar_entrada_por_nombre(TipoAgenda agenda, char nombre[]) 87 { 88 struct NodoAgenda * aux; 89 90 for (aux = agenda; aux != NULL; aux = aux->sig) 91 if (strcmp(aux->datos.nombre, nombre) == 0) 92 return aux; 93 94 return NULL; 95 } 96 332 Introducci´on a la Programaci´on con C
  • 339. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros 97 void libera_agenda(TipoAgenda agenda) 98 { 99 struct NodoAgenda * aux, *siguiente; 100 101 aux = agenda; 102 while (aux != NULL) { 103 siguiente = aux->sig; 104 libera_entrada(aux); 105 aux = siguiente; 106 } 107 } 108 109 void escribe_agenda(TipoAgenda agenda, char nombre_fichero[]) 110 { 111 struct NodoAgenda * aux; 112 FILE * fp; 113 114 fp = fopen(nombre_fichero, "w"); 115 for (aux=agenda; aux!=NULL; aux=aux->sig) 116 fprintf (fp, "%sn%sn%sn", aux->datos.nombre, 117 aux->datos.direccion, 118 aux->datos.telefono); 119 fclose(fp); 120 } 121 122 TipoAgenda lee_agenda(char nombre_fichero[]) 123 { 124 TipoAgenda agenda; 125 struct Entrada * entrada_leida; 126 FILE * fp; 127 char nombre[MAXCADENA+1], direccion[MAXCADENA+1], telefono[MAXCADENA+1]; 128 int longitud; 129 130 agenda = crea_agenda(); 131 132 fp = fopen(nombre_fichero, "r"); 133 while (1) { 134 fgets(nombre, MAXCADENA, fp); 135 if (feof (fp)) break; // Si se acab´o el fichero, acabar la lectura. 136 quita_fin_de_linea(nombre); 137 138 fgets(direccion, MAXCADENA, fp); 139 quita_fin_de_linea(direccion); 140 141 fgets(telefono, MAXCADENA, fp); 142 quita_fin_de_linea(telefono); 143 144 agenda = anyadir_entrada(agenda, nombre, direccion, telefono); 145 } 146 fclose(fp); 147 148 return agenda; 149 } 150 151 152 /************************************************************************ 153 * Programa principal 154 ************************************************************************/ 156 157 int main(void) 158 { 159 TipoAgenda miagenda; 160 struct NodoAgenda * encontrada; Introducci´on a la Programaci´on con C 333
  • 340. 5.2 Ficheros de texto 161 int opcion; 162 char nombre[MAXCADENA+1]; 163 char direccion[MAXCADENA+1]; 164 char telefono[MAXCADENA+1]; 165 char linea[MAXCADENA+1]; 166 167 miagenda = lee_agenda("agenda.txt"); 168 169 do { 170 printf ("Men´u:n"); 171 printf ("1) Ver contenido completo de la agenda.n"); 172 printf ("2) Dar de alta una persona.n"); 173 printf ("3) Buscar tel´efonos de una persona.n"); 174 printf ("4) Salir.n"); 175 printf ("Opci´on: "); 176 gets(linea); sscanf (linea, "%d", &opcion); 177 178 switch(opcion) { 179 180 case Ver: 181 muestra_agenda(miagenda); 182 break; 183 184 case Alta: 185 printf ("Nombre : "); gets(nombre); 186 printf ("Direcci´on: "); gets(direccion); 187 printf ("Tel´efono : "); gets(telefono); 188 miagenda = anyadir_entrada(miagenda, nombre, direccion, telefono); 189 break; 190 191 case Buscar: 192 printf ("Nombre: "); gets(nombre); 193 encontrada = buscar_entrada_por_nombre(miagenda, nombre); 194 if (encontrada == NULL) 195 printf ("No hay nadie llamado %s en la agenda.n", nombre); 196 else 197 muestra_entrada(encontrada); 198 break; 199 } 200 } while (opcion != Salir); 201 202 203 escribe_agenda(miagenda, "agenda.txt"); 204 libera_agenda(miagenda); 205 206 return 0; 207 } Entrada/salida de fichero para el programa de gesti´on de una colecci´on de discos Acabamos esta secci´on dedicada a los ficheros de texto con una aplicaci´on pr´actica. Vamos a a˜nadir funcionalidad al programa desarrollado en el apartado 4.11: el programa cargar´a la ((base de datos)) tan pronto inicie su ejecuci´on leyendo un fichero de texto y la guardar´a en el mismo fichero, recogiendo los cambios efectuados, al final. En primer lugar, discutamos brevemente acerca del formato del fichero de texto. Podemos almacenar cada dato en una l´ınea, as´ı: discoteca.txt 1 Expression 2 John Coltrane 3 1972 4 Ogunde 5 To be 334 Introducci´on a la Programaci´on con C
  • 341. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros 6 Offering 7 Expression 8 Number One 9 Logos 10 Tangerine Dream 11 1982 12 Logos 13 Dominion 14 Ignacio 15 Vangelis 16 1977 17 Ignacio Pero hay un serio problema: ¿c´omo sabe el programa d´onde empieza y acaba cada disco? El programa no puede distinguir entre el t´ıtulo de una canci´on, el de un disco o el nombre de un int´erprete. Podr´ıamos marcar cada l´ınea con un par de caracteres que nos indiquen qu´e tipo de informaci´on mantiene: discoteca.txt 1 TD Expression 2 IN John Coltrane 3 A~N 1972 4 TC Ogunde 5 TC To be 6 TC Offering 7 TC Expression 8 TC Number One 9 TD Logos 10 IN Tangerine Dream 11 A~N 1982 12 TC Logos 13 TC Dominion 14 TD Ignacio 15 IN Vangelis 16 A~N 1977 17 TC Ignacio Con TD indicamos ((t´ıtulo de disco)); con IN, ((int´erprete)); con A~N, ((a˜no)); y con TC, ((t´ıtulo de canci´on)). Pero esta soluci´on complica las cosas en el programa: no sabemos de qu´e tipo es una l´ınea hasta haber le´ıdo sus dos primeros caracteres. O sea, sabemos que un disco ((ha acabado)) cuando ya hemos le´ıdo una l´ınea del siguiente. No es que no se pueda trabajar as´ı, pero resulta complicado. Como podemos definir libremente el formato, optaremos por uno que preceda los t´ıtulos de las canciones por un n´umero que indique cu´antas canciones hay: discoteca.txt discoteca.txt 1 Expression 2 John Coltrane 3 1972 4 5 5 Ogunde 6 To be 7 Offering 8 Expression 9 Number One 10 Logos 11 Tangerine Dream 12 1982 13 2 14 Logos 15 Dominion 16 Ignacio 17 Vangelis 18 1977 Introducci´on a la Programaci´on con C 335
  • 342. 5.2 Ficheros de texto 19 1 20 Ignacio La lectura de la base de datos es relativamente sencilla: 1 void quita_fin_de_linea(char linea[]) 2 { 3 int i; 4 for (i=0; linea[i] != ’0’; i++) 5 if (linea[i] == ’n’) { 6 linea[i] = ’0’; 7 break; 8 } 9 } 10 11 TipoColeccion carga_coleccion(char nombre_fichero[]) 12 { 13 FILE * f; 14 char titulo_disco[MAXCAD+1], titulo_cancion[MAXCAD+1], interprete[MAXCAD+1]; 15 char linea[MAXCAD+1]; 16 int anyo; 17 int numcanciones; 18 int i; 19 TipoColeccion coleccion; 20 TipoListaCanciones lista_canciones; 21 22 coleccion = crea_coleccion(); 23 f = fopen(nombre_fichero, "r"); 24 while(1) { 25 fgets(titulo_disco, MAXCAD, f); 26 if (feof (f)) 27 break; 28 quita_fin_de_linea(titulo_disco); 29 fgets(interprete, MAXCAD, f); 30 quita_fin_de_linea(interprete); 31 fgets(linea, MAXCAD, f); sscanf (linea, "%d", &anyo); 32 fgets(linea, MAXCAD, f); sscanf (linea, "%d", &numcanciones); 33 lista_canciones = crea_lista_canciones(); 34 for (i=0; i<numcanciones; i++) { 35 fgets(titulo_cancion, MAXCAD, f); 36 quita_fin_de_linea(titulo_cancion); 37 lista_canciones = anyade_cancion(lista_canciones, titulo_cancion); 38 } 39 coleccion = anyade_disco(coleccion, titulo_disco, interprete, anyo, lista_canciones); 40 } 41 fclose(f); 42 43 return coleccion; 44 } Tan s´olo cabe rese˜nar dos cuestiones: La detecci´on del final de fichero se ha de hacer tras una lectura infructuosa, por lo que la hemos dispuesto tras el primer fgets del bucle. La lectura de l´ıneas con fgets hace que el salto de l´ınea est´e presente, as´ı que hay que eliminarlo expl´ıcitamente. Al guardar el fichero hemos de asegurarnos de que escribimos la informaci´on en el mismo formato: 1 void guarda_coleccion(TipoColeccion coleccion, char nombre_fichero[]) 2 { 3 struct Disco * disco; 336 Introducci´on a la Programaci´on con C
  • 343. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros 4 struct Cancion * cancion; 5 int numcanciones; 6 FILE * f; 7 8 f = fopen(nombre_fichero, "w"); 9 for (disco = coleccion; disco != NULL; disco = disco->sig) { 10 fprintf (f, "%sn", disco->titulo); 11 fprintf (f, "%sn", disco->interprete); 12 fprintf (f, "%dn", disco->anyo); 13 14 numcanciones = 0; 15 for (cancion = disco->canciones; cancion != NULL; cancion = cancion->sig) 16 numcanciones++; 17 fprintf (f, "%dn", numcanciones); 18 19 for (cancion = disco->canciones; cancion != NULL; cancion = cancion->sig) 20 fprintf (f, "%sn", cancion->titulo); 21 } 22 fclose(f); 23 } Observa que hemos recorrido dos veces la lista de canciones de cada disco: una para saber cu´antas canciones contiene (y as´ı poder escribir en el fichero esa cantidad) y otra para escribir los t´ıtulos de las canciones. Aqu´ı tienes las modificaciones hechas al programa principal: discoteca2 1.c discoteca2.c 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <ctype.h> . . . 253 254 255 int main(void) 256 { 257 int opcion; 258 TipoColeccion coleccion; 259 char titulo_disco[MAXCAD+1], titulo_cancion[MAXCAD+1], interprete[MAXCAD+1]; 260 char linea[MAXCAD+1]; 261 int anyo; 262 struct Disco * undisco; 263 TipoListaCanciones lista_canciones; 264 265 coleccion = carga_coleccion("discoteca.txt"); 266 267 do { 268 printf ("Men´un"); 269 printf ("1) A~nadir discon"); 270 printf ("2) Buscar por t´ıtulo del discon"); 271 printf ("3) Buscar por int´erpreten"); 272 printf ("4) Buscar por t´ıtulo de canci´onn"); 273 printf ("5) Mostrar todon"); 274 printf ("6) Eliminar un disco por t´ıtulo e int´erpreten"); 275 printf ("7) Finalizarn"); 276 printf ("Opci´on: "); gets(linea); sscanf (linea, "%d", &opcion); . . . 331 332 guarda_coleccion(coleccion, "discoteca.txt"); 333 coleccion = libera_coleccion(coleccion); 334 335 return 0; 336 } Introducci´on a la Programaci´on con C 337
  • 344. 5.2 Ficheros de texto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 319 La gesti´on de ficheros mediante su carga previa en memoria puede resultar problem´atica al trabajar con grandes vol´umenes de informaci´on. Modifica el programa de la agenda para que no cargue los datos en memoria. Todas las operaciones (a˜nadir datos y consultar) se efectuar´an gestionando directamente ficheros. · 320 Modifica el programa propuesto en el ejercicio anterior para que sea posible borrar entradas de la agenda. (Una posible soluci´on pasa por trabajar con dos ficheros, uno original y uno para copias, de modo que borrar una informaci´on sea equivalente a no escribirla en la copia.) · 321 Modifica el programa de la agenda para que se pueda mantener m´as de un tel´efono asociado a una persona. El formato del fichero pasa a ser el siguiente: Una l´ınea que empieza por la letra N contiene el nombre de una persona. Una l´ınea que empieza por la letra D contiene la direcci´on de la persona cuyo nombre acaba de aparecer. Una l´ınea que empieza por la letra T contiene un n´umero de tel´efono asociado a la persona cuyo nombre apareci´o m´as recientemente en el fichero. Ten en cuenta que no se puede asociar m´as de una direcci´on a una persona (y si eso ocurre en el fichero, debes notificar la existencia de un error), pero s´ı m´as de un tel´efono. Adem´as, puede haber l´ıneas en blanco (o formadas ´unicamente por espacios en blanco) en el fichero. He aqu´ı un ejemplo de fichero con el nuevo formato: agenda.txt 1 N Juan Gil 2 D Ronda Mijares, 1220 3 T 964 123456 4 5 N Ana Garc´ıa 6 D Plaza del Sol, 13 7 T 964-872777 8 T 964-872778 9 10 11 N Pepe P´erez 12 D Calle de Arriba, 1 13 T 964 263 263 14 T 964 163 163 15 T 96 2663 663 · 322 En un fichero matriz.mat almacenamos los datos de una matriz de enteros con el siguiente formato: La primera l´ınea contiene el n´umero de filas y columnas. Cada una de las restantes l´ıneas contiene tantos enteros (separados por espacios) como indica el n´umero de columnas. Hay tantas l´ıneas de este estilo como filas tiene la matriz. Este ejemplo define una matriz de 3 × 4 con el formato indicado: matriz.txt 1 3 4 2 1 0 3 4 3 0 -1 12 -1 4 3 0 99 -3 Escribe un programa que lea matriz.mat efectuando las reservas de memoria din´amica que corresponda y muestre por pantalla, una vez cerrado el fichero, el contenido de la matriz. 338 Introducci´on a la Programaci´on con C
  • 345. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros · 323 Modifica el programa del ejercicio anterior para que, si hay menos l´ıneas con valores de filas que filas declaradas en la primera l´ınea, se rellene el restante n´umero de filas con valores nulos. Aqu´ı tienes un ejemplo de fichero con menos filas que las declaradas: matriz incompleta.txt 1 3 4 2 1 0 3 4 · 324 Dise˜na un programa que facilite la gesti´on de una biblioteca. El programa permitir´a prestar libros. De cada libro se registrar´a al menos el t´ıtulo y el autor. En cualquier instante se podr´a volcar el estado de la biblioteca a un fichero y cargarlo de ´el. Conviene que la biblioteca sea una lista de nodos, cada uno de los cuales representa un libro. Uno de los campos del libro podr´ıa ser una cadena con el nombre del prestatario. Si dicho nombre es la cadena vac´ıa, se entender´a que el libro est´a disponible. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Permisos Unix Los ficheros Unix llevan asociados unos permisos con los que es posible determinar qu´e usuarios pueden efectuar qu´e acciones sobre cada fichero. Las acciones son: leer, escribir y ejecutar (esta ´ultima limitada a ficheros ejecutables, es decir, resultantes de una compilaci´on o que contienen c´odigo fuente de un lenguaje interpretado y siguen cierto convenio). Se puede fijar cada permiso para el usuario ((propietario)) del fichero, para los usuarios de su mismo grupo o para todos los usuarios del sistema. Cuando ejecutamos el comando ls con la opci´on -l, podemos ver los permisos codifi- cados con las letras rwx y el car´acter -: -rw-r--r-- 1 usuario migrupo 336 may 12 10:43 kk.c -rwxr-x--- 1 usuario migrupo 13976 may 12 10:43 a.out El fichero kk.c tiene permiso de lectura y escritura para el usuario (caracteres 2 a 4), de s´olo lectura para los usuarios de su grupo (caracteres 5 a 7) y de s´olo lectura para el resto de usuarios (caracteres 8 a 10). El fichero a.out puede ser le´ıdo, modificado y ejecutado por el usuario. Los usuarios del mismo grupo pueden leerlo y ejecutarlo, pero no modificar su contenido. El resto de usuarios no puede acceder al fichero. El comando Unix chmod permite modificar los permisos de un fichero. Una forma tradi- cional de hacerlo es con un n´umero octal que codifica los permisos. Aqu´ı tienes un ejemplo de uso: $ chown 0700 a.out $ ls -l a.out -rwx------ 1 usuario migrupo 13976 may 12 10:43 a.out El valor octal 0700 (que en binario es 111000000), por ejemplo, otorga permisos de lectura, escritura y ejecuci´on al propietario del fichero, y elimina cualquier permiso para el resto de usuarios. De cada 3 bits, el primero fija el permiso de lectura, el segundo el de escritura y el tercero el de ejecuci´on. Los 3 primeros bits corresponden al usuario, los tres siguientes al grupo y los ´ultimos 3 al resto. As´ı pues, 0700 equivale a -rwx------ en la notaci´on de ls -l. Por ejemplo, para que a.out sea tambi´en legible y ejecutable por parte de cualquier miembro del grupo del propietario puedes usar el valor 0750 (que equivale a -rwxr-x---). 5.2.3. Los ((ficheros)) de consola Hay tres ficheros de texto predefinidos y ya abiertos cuando se inicia un programa: los ((ficheros)) de consola. En realidad, no son ficheros, sino dispositivos: stdin (entrada est´andar): el teclado; stdout (salida est´andar): la pantalla; Introducci´on a la Programaci´on con C 339
  • 346. 5.2 Ficheros de texto stderr (salida est´andar de error): ¿? ¿Qu´e es stderr? En principio es tambi´en la pantalla, pero podr´ıa ser, por ejemplo un fichero en el que deseamos llevar un cuaderno de bit´acora con las anomal´ıas o errores detectados durante la ejecuci´on del programa. La funci´on printf es una forma abreviada de llamar a fprintf sobre stdout y scanf encubre una llamada a fscanf sobre stdin. Por ejemplo, estas dos llamadas son equivalentes: printf ("Esto es la %sn", "pantalla"); f printf (stdout, "Esto es la %sn", "pantalla"); El hecho de que, en el fondo, Unix considere al teclado y la pantalla equivalentes a ficheros nos permite hacer ciertas cosas curiosas. Por ejemplo, si deseamos ejecutar un programa cuyos datos se deben leer de teclado o de fichero, seg´un convenga, podemos decidir la fuente de entrada en el momento de la ejecuci´on del programa. Este programa, por ejemplo, permite elegir al usuario entre leer de teclado o leer de fichero: selecciona entrada.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 FILE * fp; 6 char dedonde[80], nombre[80]; 7 int n; 8 9 printf ("Leo de fichero o de teclado (f/t)?: "); 10 gets(dedonde); 11 if (dedonde[0] == ’f’) { 12 printf ("Nombre del fichero: "); 13 gets(nombre); 14 fp = fopen(nombre, "r"); 15 } 16 else 17 fp = stdin; 18 19 ... 20 fscanf (fp, "%d", &n); /* Lee de fichero o teclado. */ 21 ... 22 if (fp != stdin) 23 fclose(fp); 24 ... 25 26 return 0; 27 } Existe otra forma de trabajar con fichero o teclado que es m´as c´omoda para el programador: usando la capacidad de redirecci´on que facilita el int´erprete de comandos Unix. La idea consiste en desarrollar el programa considerando s´olo la lectura por teclado y, cuando iniciamos la ejecu- ci´on del programa, redirigir un fichero al teclado. Ahora ver´as c´omo. F´ıjate en este programa: pares.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int i, n; 6 7 for (i=0; i<10; i++) { 8 scanf ("%d", &n); 9 if (n%2==0) 10 printf ("[%d]", n); 11 } 340 Introducci´on a la Programaci´on con C
  • 347. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros De cadena a entero o flotante Los ficheros de texto contienen eso, texto. No obstante, el texto se interpreta en ocasiones como si codificara valores enteros o flotantes. La funci´on fscanf , por ejemplo, es capaz de leer texto de un fichero e interpretarlo como si fuera un entero o un flotante. Cuando hacemos fscanf (f, "%d", &a), donde a es de tipo int, se leen caracteres del fichero y se interpretan como un entero. Pero hay un problema potencial: el texto puede no corresponder a un valor entero, con lo que la lectura no se efectuar´ıa correctamente. Una forma de curarse en salud es leer como cadena los siguientes caracteres (con fscanf y la marca de formato %s o con gets, por ejemplo), comprobar que la secuencia de caracteres le´ıda describe un entero (o un flotante, seg´un convenga) y convertir ese texto en un entero (o flotante). ¿C´omo efectuar la conversi´on? C nos ofrece en su biblioteca est´andar la funci´on atoi, que recibe una cadena y devuelve un entero. Has de incluir la cabecera stdlib.h para usarla. Aqu´ı tienes un ejemplo de uso: 1 #include <stdio.h> 2 #include <stdlib.h> 3 4 int main(void) 5 { 6 char a[] = "123"; 7 int b; 8 9 b = atoi(a); 10 11 printf ("La cadena %s se interpreta como el entero %d con atoin", a, b); 12 13 return 0; 14 } Si deseas interpretar el texto como un float, puedes usar atof en lugar de atoi. As´ı de f´acil. 12 13 return 0; 14 } Si lo compilas para generar un programa pares, lo ejecutas e introduces los siguientes 10 n´umeros enteros, obtendr´as este resultado en pantalla: $ pares 3 5 6 [6] 7 2 [2] 10 [10] 2 [2] 1 3 13 Cada vez que el ordenador ha detectado un n´umero par, lo ha mostrado en pantalla entre corchetes. Creemos ahora, con la ayuda de un editor de texto, numeros.txt, un fichero de texto con los mismos 10 n´umeros enteros que hemos introducido por teclado antes: numeros.txt 1 3 Introducci´on a la Programaci´on con C 341
  • 348. 5.2 Ficheros de texto 2 5 3 6 4 7 5 2 6 10 7 2 8 1 9 3 10 13 Podemos llamar a pares as´ı: $ pares < numeros.txt [6] [2] [10] [2] El car´acter < indica a Unix que lea del fichero numeros.txt en lugar de leer del teclado. El programa, sin tocar una sola l´ınea, pasa a leer los valores de numeros.txt y muestra por pantalla los que son pares. Tambi´en podemos redirigir la salida (la pantalla) a un fichero. F´ıjate: $ pares < numeros.txt > solopares.txt Ahora el programa se ejecuta sin mostrar texto alguno por pantalla y el fichero solopares.txt acaba conteniendo lo que debiera haberse mostrado por pantalla. $ cat solopares.txt [6] [2] [10] [2] Para redirigir la salida de errores, puedes usar el par de caracteres 2> seguido del nombre del fichero en el que se escribir´an los mensajes de error. La capacidad de redirigir los dispositivos de entrada, salida y errores tiene infinidad de apli- caciones. Una evidente es automatizar la fase de pruebas de un programa durante su desarrollo. En lugar de escribir cada vez todos los datos que solicita un programa para ver si efect´ua correc- tamente los c´alculos, puedes preparar un fichero con los datos de entrada y utilizar redirecci´on para que el programa los lea autom´aticamente. 5.2.4. Un par de utilidades Hemos aprendido a crear ficheros y a modificar su contenido. No sabemos, sin embargo, c´omo eliminar un fichero del sistema de ficheros ni c´omo rebautizarlo. Hay dos funciones de la librer´ıa est´andar de C (accesibles al incluir stdio.h) que permiten efectuar estas dos operaciones: remove: elimina el fichero cuya ruta se proporciona. int remove(char ruta[]); La funci´on devuelve 0 si se consigui´o eliminar el fichero y otro valor si se cometi´o alg´un error. ¡Ojo! No confundas borrar un fichero con borrar el contenido de un fichero. La funci´on remove elimina completamente el fichero. Abrir un fichero en modo escritura y cerrarlo inmediatamente elimina su contenido, pero el fichero sigue existiendo (ocupando, eso s´ı, 0 bytes). rename: cambia el nombre de un fichero. int rename(char ruta_original[], char nueva_ruta[]); La funci´on devuelve 0 si no hubo error, y otro valor en caso contrario. 342 Introducci´on a la Programaci´on con C
  • 349. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros Los peligros de gets. . . y c´omo superarlos Ya habr´as podido comprobar que gets no es una funci´on segura, pues siempre es posible desbordar la memoria reservada leyendo una cadena suficientemente larga. Algunos com- piladores generan una advertencia cuando detectan el uso de gets. ¿C´omo leer, pues, una l´ınea de forma segura? Una posibilidad consiste en escribir nuestra propia funci´on de lectura car´acter a car´acter (con ayuda de la funci´on fgetc) e imponer una limitaci´on al n´umero de caracteres le´ıdos. 1 int lee_linea(char linea[], int max_lon) 2 { 3 int c, nc = 0; 4 max_lon--; /* Se reserva un car´acter para el 0 */ 5 6 while ( (c = fgetc(stdin)) != EOF ) { 7 if (c == ’n’) 8 break; 9 if (nc < max_lon) 10 linea[nc++] = c; 11 } 12 13 if (c == EOF && nc == 0) 14 return EOF; 15 16 linea[nc] = ’0’; 17 return nc; 18 } Para leer una cadena en un vector de caracteres con una capacidad m´axima de 100 caracteres, haremos: lee_linea(cadena, 100); El valor de cadena se modificar´a para contener la cadena le´ıda. La cadena m´as larga le´ıda tendr´a una longitud de 99 caracteres (recuerda que el ’0’ ocupa uno de los 100). Pero hay una posibilidad a´un m´as sencilla: usar fgets sobre stdin: fgets(cadena, 100, stdin); Una salvedad: fgets incorpora a la cadena le´ıda el salto de l´ınea, cosa que gets no hace. La primera versi´on, no obstante, sigue teniendo inter´es, pues te muestra un ((esqueleto)) de funci´on ´util para un control detallado de la lectura por teclado. Inspir´andote en ella puedes escribir, por ejemplo, una funci´on que s´olo lea d´ıgitos, o letras, o texto que satisface alguna determinada restricci´on. La consulta de teclado La funci´on getc (o, para el caso, fgetc actuando sobre stdin) bloquea la ejecuci´on del programa hasta que el usuario teclea algo y pulsa la tecla de retorno. Muchos programadores se preguntan ¿c´omo puedo saber si una tecla est´a pulsada o no sin quedar bloqueado? Ciertas aplicaciones, como los videojuegos, necesitan efectuar consultas al estado del teclado no bloqueantes. Malas noticias: no es un asunto del lenguaje C, sino de bibliotecas espec´ıficas. El C est´andar nada dice acerca de c´omo efectuar esa operaci´on. En Unix, la biblioteca curses, por ejemplo, permite manipular los terminales y acceder de diferentes modos al teclado. Pero no es una biblioteca f´acil de (aprender a) usar. Y, adem´as, presenta problemas de portabilidad, pues no necesariamente est´a disponible en todos los sistemas operativos. Cosa parecida podemos decir de otras cuestiones: sonido, gr´aficos tridimensionales, in- terfaces gr´aficas de usuario, etc. C, en tanto que lenguaje de programaci´on estandarizado, no ofrece soporte. Eso s´ı: hay bibliotecas para infinidad de campos de aplicaci´on. Tendr´as que encontrar la que mejor se ajusta a tus necesidades y. . . ¡estudiar! Introducci´on a la Programaci´on con C 343
  • 350. 5.3 Ficheros binarios 5.3. Ficheros binarios 5.3.1. Abrir, leer/escribir, cerrar La gesti´on de ficheros binarios obliga a trabajar con el mismo protocolo b´asico: 1. abrir el fichero en el modo adecuado, 2. leer y/o escribir informaci´on, 3. y cerrar el fichero. La funci´on de apertura de un fichero binario es la misma que hemos usado para los ficheros de texto: fopen. Lo que cambia es el modo de apertura: debe contener la letra b. Los modos de apertura b´asicos3 para ficheros binarios son, pues: "rb" (lectura): El primer byte le´ıdo es el primero del fichero. "wb" (escritura): Trunca el fichero a longitud 0. Si el fichero no existe, se crea. "ab" (adici´on): Es un modo de escritura que preserva el contenido original del fichero. Los datos escritos se a˜naden al final del fichero. Si el fichero no puede abrirse por cualquier raz´on, fopen devuelve el valor NULL. La funci´on de cierre del fichero es fclose. Las funciones de lectura y escritura s´ı son diferentes: fread: recibe una direcci´on de memoria, el n´umero de bytes que ocupa un dato, el n´umero de datos a leer y un fichero. He aqu´ı su prototipo4 : int fread( void * direccion, int tam, int numdatos, FILE * fichero ); Los bytes le´ıdos se almacenan a partir de direccion. Devuelve el n´umero de datos que ha conseguido leer (y si ese valor es menor que numdatos, es porque hemos llegado al final del fichero y no se ha podido efectuar la lectura completa). fwrite: recibe una direcci´on de memoria, el n´umero de bytes que ocupa un dato, el n´umero de datos a escribir y un fichero. Este es su prototipo: int fwrite( void * direccion, int tam, int numdatos, FILE * fichero ); Escribe en el fichero los tam por numdatos bytes existentes desde direccion en adelante. Devuelve el n´umero de datos que ha conseguido escribir (si vale menos que numdatos, hubo alg´un error de escritura). Empezaremos a comprender c´omo trabajan estas funciones con un sencillo ejemplo. Vamos a escribir los diez primeros n´umeros enteros en un fichero: diez enteros.c diez enteros.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 FILE * fp; 6 int i; 7 8 fp = fopen("primeros.dat", "wb"); 9 for (i=0; i<10; i++) 10 fwrite(&i, sizeof(int), 1, fp); 11 fclose(fp); 12 13 return 0; 14 } 3M´as adelante te presentamos tres modos de apertura adicionales. 4Bueno, casi. El prototipo no usa el tipo int, sino size t, que est´a definido como unsigned int. Preferimos presentarte una versi´on modificada del prototipo para evitar introducir nuevos conceptos. 344 Introducci´on a la Programaci´on con C
  • 351. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros Analicemos la llamada a fwrite. F´ıjate: pasamos la direcci´on de memoria en la que empieza un entero (con &i) junto al tama˜no en bytes de un entero (sizeof(int), que vale 4) y el valor 1. Estamos indicando que se van a escribir los 4 bytes (resultado de multiplicar 1 por 4) que empiezan en la direcci´on &i, es decir, se va a guardar en el fichero una copia exacta del contenido de i. Quiz´a entiendas mejor qu´e ocurre con esta otra versi´on capaz de escribir un vector completo en una sola llamada a fwrite: 1 #include <stdio.h> 2 3 int main(void) 4 { 5 FILE * fp; 6 int i, v[10]; 7 8 for (i=0; i<10; i++) 9 v[i] = i; 10 fp = fopen("primeros.dat", "wb"); 11 fwrite(v, sizeof(int), 10, fp); 12 fclose(fp); 13 14 return 0; 15 } Ahora estamos pasando la direcci´on en la que empieza un vector (v es una direcci´on, as´ı que no hemos de poner un & delante), el tama˜no de un elemento del vector (sizeof(int)) y el n´umero de elementos del vector (10). El efecto es que se escriben en el fichero los 40 bytes de memoria que empiezan donde empieza v. Resultado: todo el vector se almacena en disco con una sola operaci´on de escritura. C´omodo, ¿no? Ya te dijimos que la informaci´on de todo fichero binario ocupa exactamente el mismo n´umero de bytes que ocupar´ıa en memoria. Hagamos la prueba. Veamos con ls -l, desde el int´erprete de comandos de Unix, cu´anto ocupa el fichero: $ ls -l primeros.dat -rw-r--r-- 1 usuario migrupo 40 may 10 11:00 primeros.dat Efectivamente, ocupa exactamente 40 bytes (el n´umero que aparece en quinto lugar). Si lo mostramos con cat, no sale nada con sentido en pantalla. $ cat primeros.dat $ ¿Por qu´e? Porque cat interpreta el fichero como si fuera de texto, as´ı que encuentra la siguiente secuencia binaria: 1 00000000 00000000 00000000 00000000 2 00000000 00000000 00000000 00000001 3 00000000 00000000 00000000 00000010 4 00000000 00000000 00000000 00000011 5 00000000 00000000 00000000 00000100 6 ... Los valores ASCII de cada grupo de 8 bits no siempre corresponden a caracteres visibles, por lo que no se representan como s´ımbolos en pantalla (no obstante, algunos bytes s´ı tienen efecto en pantalla; por ejemplo, el valor 9 corresponde en ASCII al tabulador). Hay una herramienta Unix que te permite inspeccionar un fichero binario: od (abreviatura de ((octal dump)), es decir, ((volcado octal))). $ od -l primeros.dat 0000000 0 1 2 3 0000020 4 5 6 7 0000040 8 9 0000050 Introducci´on a la Programaci´on con C 345
  • 352. 5.3 Ficheros binarios (La opci´on -l de od hace que muestre la interpretaci´on como enteros de grupos de 4 bytes.) ¡Ah´ı est´an los n´umeros! La primera columna indica (en hexadecimal) el n´umero de byte del primer elemento de la fila. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 325 ¿Qu´e aparecer´a en pantalla si mostramos con el comando cat el contenido del fichero binario otraprueba.dat generado en este programa?: otra prueba.c otra prueba.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 FILE * fp; 6 int i, v[26]; 7 8 fp = fopen("otra prueba.dat", "wb"); 9 for (i=97; i<123; i++) 10 v[i-97] = i; 11 fwrite(v, sizeof(int), 26, fp); 12 fclose(fp); 13 14 return 0; 15 } (Una pista: el valor ASCII del car´acter ’a’ es 97.) ¿Y qu´e aparecer´a si lo visualizas con el comando od -c (la opci´on -c indica que se desea ver el fichero car´acter a car´acter e interpretado como secuencia de caracteres). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ya puedes imaginar c´omo se leen datos de un fichero binario: pasando la direcci´on de me- moria en la que queremos que se copie cierta cantidad de bytes del fichero. Los dos programas siguientes, por ejemplo, leen los diez valores escritos en los dos ´ultimos programas. El primero lee entero a entero (de 4 bytes en 4 bytes), y el segundo con una sola operaci´on de lectura (cargando los 40 bytes de golpe): lee primeros.c lee primeros.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 FILE * fp; 6 int i, n; 7 8 fp = fopen("primeros.dat", "rb"); 9 for (i=0; i<10; i++) { 10 fread(&n, sizeof(int), 1, fp); 11 printf ("%dn", n); 12 } 13 fclose(fp); 14 15 return 0; 16 } lee primeros2.c lee primeros2.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 FILE * fd; 6 int i, v[10]; 7 8 fp = fopen("primeros.dat", "rb"); 346 Introducci´on a la Programaci´on con C
  • 353. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros 9 fread(v, sizeof(int), 10, fp); 10 for (i=0; i<10; i++) 11 printf ("%dn", v[i]); 12 fclose(fp); 13 14 return 0; 15 } En los dos programas hemos indicado expl´ıcitamente que ´ıbamos a leer 10 enteros, pues sab´ıamos de antemano que hab´ıa exactamente 10 n´umeros en el fichero. Es f´acil modificar el primer programa para que lea tantos enteros como haya, sin conocer a priori su n´umero: lee todos.c lee todos.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 FILE * fp; 6 int n; 7 8 fp = fopen("primeros.dat", "rb"); 9 fread(&n, sizeof(int), 1, fp); 10 while (!feof (fp)) { 11 printf ("%dn", n); 12 fread(&n, sizeof(int), 1, fp); 13 } 14 fclose(fp); 15 16 return 0; 17 } Lo cierto es que hay una forma m´as idiom´atica, m´as com´un en C de expresar lo mismo: lee todos2.c lee todos2.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 FILE * fp; 6 int n; 7 8 f = fopen("primeros.dat", "rb"); 9 while ( fread(&n, sizeof(int), 1, fp) == 1 ) 10 printf ("%dn", n); 11 fclose(fp); 12 13 return 0; 14 } En esta ´ultima versi´on, la lectura de cada entero se efect´ua con una llamada a fread en la condici´on del while. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 326 Dise˜na un programa que genere un fichero binario primos.dat con los 1000 primeros n´umeros primos. · 327 Dise˜na un programa que a˜nada al fichero binario primos.dat (ver ejercicio anterior) los 100 siguientes n´umeros primos. El programa leer´a el contenido actual del fichero para averiguar cu´al es el ´ultimo primo conocido. A continuaci´on, abrir´a el fichero en modo adici´on y a˜nadir´a 100 nuevos primos. Si ejecut´asemos dos veces el programa, el fichero acabar´ıa conteniendo los 1200 primeros primos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . No s´olo puedes guardar tipos relativamente elementales. Tambi´en puedes almacenar en disco tipos de datos creados por ti. Este programa, por ejemplo, lee de disco un vector de puntos, lo modifica y escribe en el fichero el contenido del vector: Introducci´on a la Programaci´on con C 347
  • 354. 5.3 Ficheros binarios escribe registro.c escribe registro.c 1 #include <stdio.h> 2 #include <math.h> 3 4 struct Punto { 5 float x; 6 float y; 7 }; 8 9 int main(void) 10 { 11 FILE * fp; 12 struct Punto v[10]; 13 int i; 14 15 // Cargamos en memoria un vector de puntos. 16 fp = fopen("puntos.dat", "rb"); 17 fread(v, sizeof(struct Punto), 10, fp); 18 fclose(fp); 19 20 // Procesamos los puntos (calculamos el valor absoluto de cada coordenada). 21 for (i=0; i<10; i++) { 22 v[i].x = fabs(v[i].x); 23 v[i].y = fabs(v[i].y); 24 } 25 26 // Escribimos el resultado en otro fichero. 27 fp = fopen("puntos2.dat", "wb"); 28 fwrite(v, sizeof(struct Punto), 10, fp); 29 fclose(fp); 30 31 return 0; 32 } Esta otra versi´on no carga el contenido del primer fichero completamente en memoria en una primera fase, sino que va leyendo, procesando y escribiendo punto a punto: 1 #include <stdio.h> 2 #include <math.h> 3 4 struct Punto { 5 float x; 6 float y; 7 }; 8 9 int main(void) 10 { 11 FILE * fp_entrada, * fp_salida; 12 struct Punto p; 13 int i; 14 15 fp_entrada = fopen("puntos.dat", "rb"); 16 fp_salida = fopen("puntos2.dat", "wb"); 17 18 for (i=0; i<10; i++) { 19 fread(&p, sizeof(struct Punto), 1, fp_entrada); 20 p.x = fabs(p.x); 21 p.y = fabs(p.y); 22 fwrite(&p, sizeof(struct Punto), 1, fp_salida); 23 } 24 fclose(fp_entrada); 25 fclose(fp_salida); 26 27 return 0; 348 Introducci´on a la Programaci´on con C
  • 355. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros 28 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 328 Los dos programas anteriores suponen que hay diez puntos en el fichero puntos.dat. Modif´ıcalos para que procesen tantos puntos como haya en el fichero. · 329 Implementa un programa que genere un fichero llamado puntos.dat con 10 elementos del tipo struct Punto. Las coordenadas de cada punto se generar´an aleatoriamente en el rango [−10, 10]. Usa el ´ultimo programa para generar el fichero puntos2.dat. Comprueba que contiene el valor absoluto de los valores de puntos.dat. Si es necesario, dise˜na un nuevo programa que muestre por pantalla el contenido de un fichero de puntos cuyo nombre suministra por teclado el usuario. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.3.2. Acceso directo Los ficheros binarios pueden utilizarse como ((vectores en disco)) y acceder directamente a cualquier elemento del mismo. Es decir, podemos abrir un fichero binario en modo ((lectura- escritura)) y, gracias a la capacidad de desplazarnos libremente por ´el, leer/escribir cualquier dato. Es como si dispusieras del control de avance r´apido hacia adelante y hacia atr´as de un reproductor/grabador de cintas magnetof´onicas. Con ´el puedes ubicar el ((cabezal)) de lectu- ra/escritura en cualquier punto de la cinta y pulsar el bot´on ((play)) para escuchar (leer) o el bot´on ((record)) para grabar (escribir). Adem´as de los modos de apertura de ficheros binarios que ya conoces, puedes usar tres modos de lectura/escritura adicionales: "r+b": No se borra el contenido del fichero, que debe existir previamente. El ((cabezal)) de lectura/escritura se sit´ua al principio del fichero. "w+b": Si el fichero no existe, se crea, y si existe, se trunca el contenido a longitud cero. El ((cabezal)) de lectura/escritura se sit´ua al principio del fichero. "a+b": Si el fichero no existe, se crea. El ((cabezal)) de lectura/escritura se sit´ua al final del fichero. Para poder leer/escribir a voluntad en cualquier posici´on de un fichero abierto en alguno de los modos binarios necesitar´as dos funciones auxiliares: una que te permita desplazarte a un punto arbitrario del fichero y otra que te permita preguntar en qu´e posici´on te encuentras en un instante dado. La primera de estas funciones es fseek, que desplaza el ((cabezal)) de lectura/escritura al byte que indiquemos. int fseek(FILE * fp, int desplazamiento, int desde_donde); El valor desde_donde se fija con una constante predefinida que proporciona una interpretaci´on distinta a desplazamiento: SEEK_SET: el valor de desplazamiento es un valor absoluto a contar desde el principio del fichero. Por ejemplo, fseek(fp, 3, SEEK_SET) desplaza al cuarto byte del fichero fp. (La posici´on 0 corresponde al primer byte del fichero.) SEEK_CUR: el valor de desplazamiento es un valor relativo al lugar en que nos encontramos en un instante dado. Por ejemplo, si nos encontramos en el cuarto byte del fichero fp, la lla- mada fseek(fp, -2, SEEK_CUR) nos desplazar´a al segundo byte, y fseek(fp, 2, SEEK_CUR) al sexto. SEEK_END: el valor de desplazamiento es un valor absoluto a contar desde el final del fichero. Por ejemplo, fseek(fp, -1, SEEK_END) nos desplaza al ´ultimo byte de fp: si a continuaci´on ley´esemos un valor, ser´ıa el del ´ultimo byte del fichero. La llamada fseek(fp, 0, SEEK_END) nos situar´ıa fuera del fichero (en el mismo punto en el que estamos si abrimos el fichero en modo de adici´on). Introducci´on a la Programaci´on con C 349
  • 356. 5.3 Ficheros binarios La funci´on devuelve el valor 0 si tiene ´exito, y un valor no nulo en caso contrario. Has de tener siempre presente que los desplazamientos sobre el fichero se indican en bytes. Si hemos almacenado enteros de tipo int en un fichero binario, deberemos tener la precauci´on de que todos nuestros fseek tengan desplazamientos m´ultiplos de sizeof(int). Este programa, por ejemplo, pone a cero todos los valores pares de un fichero binario de enteros: anula pares.c anula pares.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 FILE * fp; 6 int n, bytes_leidos, cero = 0; 7 8 fp = fopen("fichero.dat", "r+b"); 9 while (fread(&n, sizeof(int), 1, fp) != 0) { 10 if (n % 2 == 0) { // Si el ´ultimo valor le´ıdo es par... 11 fseek(fp, -sizeof(int), SEEK_CUR); // ... damos un paso atr´as ... 12 fwrite(&cero, sizeof(int), 1, fp); // ... y sobreescribimos su valor absoluto. 13 } 14 } 15 fclose(fp); 16 17 return 0; 18 } La segunda funci´on que te presentamos en este apartado es ftell. Este es su prototipo: int ftell(FILE *fp); El valor devuelto por la funci´on es la posici´on en la que se encuentra el ((cabezal)) de lectu- ra/escritura en el instante de la llamada. Veamos un ejemplo. Este programa, por ejemplo, crea un fichero y nos dice el n´umero de bytes del fichero: cuenta bytes.c cuenta bytes.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 FILE * fp; 6 int i, pos; 7 8 fp = fopen("prueba.dat", "wb"); 9 for (i=0; i<10; i++) 10 fwrite(&i, sizeof(int), 1, fp); 11 fclose(fp); 12 13 fp = fopen("prueba.dat", "rb"); 14 fseek(fp, 0, SEEK_END); 15 pos = ftell(fp); 16 printf ("Tama~no del fichero: %dn", pos); 17 fclose(fp); 18 19 return 0; 20 } F´ıjate bien en el truco que permite conocer el tama˜no de un fichero: nos situamos al final del fichero con ftell indicando que queremos ir al ((primer byte desde el final)) (byte 0 con el modo SEEK_END) y averiguamos a continuaci´on la posici´on en la que nos encontramos (valor devuelto por ftell). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 330 Dise˜na una funci´on de nombre rebobina que recibe un FILE * y nos ubica al inicio del mismo. 350 Introducci´on a la Programaci´on con C
  • 357. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros · 331 Dise˜na una funci´on que reciba un FILE * (ya abierto) y nos diga el n´umero de bytes que ocupa. Al final, la funci´on debe dejar el cursor de lectura/escritura en el mismo lugar en el que estaba cuando se la llam´o. · 332 Dise˜na un programa que calcule y muestre por pantalla el m´aximo y el m´ınimo de los valores de un fichero binario de enteros. · 333 Dise˜na un programa que calcule el m´aximo de los enteros de un fichero binario y lo intercambie por el que ocupa la ´ultima posici´on. · 334 Nos pasan un fichero binario dobles.dat con una cantidad indeterminada de n´umeros de tipo float. Sabemos, eso s´ı, que los n´umeros est´an ordenados de menor a mayor. Dise˜na un programa que pida al usuario un n´umero y determine si est´a o no est´a en el fichero. En una primera versi´on, implementa una b´usqueda secuencial que se detenga tan pronto est´es seguro de que el n´umero buscado est´a o no. El programa, en su versi´on final, deber´a efectuar la b´usqueda dicot´omicamente (en un cap´ıtulo anterior se ha explicado qu´e es una b´usqueda dicot´omica). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Trabajar con ficheros binarios como si se tratara de vectores tiene ciertas ventajas, pero tambi´en inconvenientes. La ventaja m´as obvia es la capacidad de trabajar con cantidades in- gentes de datos sin tener que cargarlas completamente en memoria. El inconveniente m´as serio es la enorme lentitud con que se pueden ejecutar entonces los programas. Ten en cuenta que desplazarse por un fichero con fseek obliga a ubicar el ((cabezal)) de lectura/escritura del disco duro, una operaci´on que es intr´ınsecamente lenta por comportar operaciones mec´anicas, y no s´olo electr´onicas. Si en un fichero binario mezclas valores de varios tipos resultar´a dif´ıcil, cuando no imposible, utilizar sensatamente la funci´on fseek para posicionarse en un punto arbitrario del fichero. Tenemos un problema similar cuando la informaci´on que guardamos en un fichero es de longitud intr´ınsecamente variable. Pongamos por caso que usamos un fichero binario para almacenar una lista de palabras. Cada palabra es de una longitud, as´ı que no hay forma de saber a priori en qu´e byte del fichero empieza la n-´esima palabra de la lista. Un truco consiste en guardar cada palabra ocupando tanto espacio como la palabra m´as larga. Este programa, por ejemplo, pide palabras al usuario y las escribe en un fichero binario en el que todas las cadenas miden exactamente lo mismo (aunque la longitud de cada una de ellas sea diferente): guarda palabras.c guarda palabras.c 1 #include <stdio.h> 2 3 #define MAXLON 80 4 5 int main(void) 6 { 7 char palabra[MAXLON+1], seguir[MAXLON+1]; 8 FILE * fp; 9 10 fp = fopen("diccio.dat", "wb"); 11 do { 12 printf ("Dame una palabra: "); gets(palabra); 13 fwrite(palabra, sizeof(char), MAXLON, fp); 14 printf ("Pulsa ’s’ para a~nadir otra."); gets(seguir); 15 } while (strcmp(seguir, "s") == 0); 16 fclose(fp); 17 18 return 0; 19 } F´ıjate en que cada palabra ocupa siempre lo mismo, independientemente de su longitud: 80 bytes. Este otro programa es capaz ahora de mostrar la lista de palabras en orden inverso, gracias a la ocupaci´on fija de cada palabra: lee palabras orden inverso.c lee palabras orden inverso.c 1 #include <stdio.h> 2 3 #define MAXLON 80 Introducci´on a la Programaci´on con C 351
  • 358. 5.3 Ficheros binarios Ficheros binarios en Python Python tambi´en permite trabajar con ficheros binarios. La apertura, lectura/escritura y cierre de ficheros se efect´ua con las funciones y m´etodos de Python que ya conoces: open, read, write y close. Con read puedes leer un n´umero cualquiera de caracteres (de bytes) en una cadena. Por ejemplo, f.read(4) lee 4 bytes del fichero f (previamente abierto con open). Si esos 4 bytes corresponden a un entero (en binario), la cadena contiene 4 caracteres que lo codifican (aunque no de forma que los podamos visualizar c´omodamente). ¿C´omo asignamos a una variable el valor entero codificado en esa cadena? Python proporciona un m´odulo con funciones que permiten pasar de binario a ((tipos Python)) y viceversa: el m´odulo struct. Su funci´on unpack ((desempaqueta)) informaci´on binaria de una cadena. Para ((desempaquetar)) un entero de una cadena almacenada en una variable llamada enbinario la llamamos as´ı: unpack("i", enbinario). El primer par´ametro desempe˜na la misma funci´on que las cadenas de formato en scanf , s´olo que usa un juego de marcas de formato diferentes (i para el equivalente a un int, d para float, q para long long, etc.. Consulta el manual del m´odulo struct para conocerlos.). Aqu´ı tienes un ejemplo de uso: un programa que lee y muestra los valores de un fichero binario de enteros: 1 from struct import unpack 2 f = open("primeros.dat", "r") 3 while 1: 4 c = f.read(4) 5 if c == ’’: break 6 v = unpack("i", c) 7 print v[0] 8 f.close() F´ıjate en que el valor devuelto por unpack no es directamente el entero, sino una lista (en realidad una tupla), por lo que es necesario indexarla para acceder al valor que nos interesa. La raz´on de que devuelva una lista es que unpack puede desempaquetar varios valores a la vez. Por ejemplo, unpack("iid", cadena) desempaqueta dos enteros y un flotante de cadena (que debe tener al menos 16 bytes, claro est´a). Puedes asignar los valores devueltos a tres variables as´ı: a, b, c = unpack("iid", cadena). Hemos aprendido, pues, a leer ficheros binarios con Python. ¿C´omo los escribimos? Siguiendo un proceso inverso: empaquetando primero nuestros ((valores Python)) en cadenas que los codifican en binario mediante la funci´on pack y escribiendolas con el m´etodo write. Este programa de ejemplo escribe un fichero binario con los n´umeros del 0 al 99: 1 from struct import pack 2 f = open("primeros.dat", "w") 3 for v in range(100): 4 c = pack("i", v) 5 f.write(c) 6 f.close() S´olo queda que aprendas a implementar acceso directo a los ficheros binarios con Python. Tienes disponibles los modos de apertura ’r+’, ’w+’ y ’a+’. Adem´as, el m´etodo seek permite desplazarse a un byte cualquiera del fichero y el m´etodo tell indica en qu´e posici´on del fichero nos encontramos. 4 5 int main(void) 6 { 7 FILE * fp; 8 char palabra[MAXLON+1]; 9 int tam; 10 11 /* primero, averiguar el tama˜no del fichero (en palabras) */ 12 fp = fopen("diccio.dat", "rb"); 13 tam = fseek(fp, 0, SEEK_END) / MAXLON; 14 15 /* y ya podemos listarlas en orden inverso */ 352 Introducci´on a la Programaci´on con C
  • 359. CC 2003, 2008 Andr´es Marzal e Isabel Gracia 5 Ficheros 16 for (i=tam-1; i>=0; i--) { 17 fseek(fp, i * MAXLON, SEEK_SET); 18 fread(palabra, sizeof(char), MAXLON, fp); 19 printf ("%sn", palabra); 20 } 21 fclose(fp); 22 23 return 0; 24 } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 335 Los dos programas anteriores pueden plantear problemas cuando trabajan con palabras que tienen 80 caracteres m´as el terminador. ¿Qu´e problemas? ¿C´omo los solucionar´ıas? · 336 Dise˜na un programa que lea una serie de valores enteros y los vaya escribiendo en un fichero hasta que el usuario introduzca el valor −1 (que no se escribir´a en el fichero). Tu programa debe, a continuaci´on, determinar si la secuencia de n´umeros introducida en el fichero es pal´ındroma. · 337 Deseamos gestionar una colecci´on de c´omics. De cada c´omic anotamos los siguientes datos: Superh´eroe: una cadena de hasta 20 caracteres. T´ıtulo: una cadena de hasta 200 caracteres. N´umero: un entero. A˜no: un entero. Editorial: una cadena de hasta 30 caracteres. Sinopsis: una cadena de hasta 1000 caracteres. El programa permitir´a: 1. Dar de alta un c´omic. 2. Consultar la ficha completa de un c´omic dado el superh´eroe y el n´umero del episodio. 3. Ver un listado por superh´eroe que muestre el t´ıtulo de todas sus historias. 4. Ver un listado por a˜no que muestre el superh´erore y t´ıtulo de todas sus historias. Dise˜na un programa que gestione la base de datos teniendo en cuenta que no queremos cargarla en memoria cada vez que ejecutamos el programa, sino gestionarla directamente sobre disco. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.4. Errores Algunas de las operaciones con ficheros pueden resultar fallidas (apertura de un fichero cuya ruta no apunta a ning´un fichero existente, cierre de un fichero ya cerrado, etc.). Cuando as´ı ocurre, la funci´on llamada devuelve un valor que indica que se cometi´o un error, pero ese valor s´olo no aporta informaci´on que nos permita conocer el error cometido. La informaci´on adicional est´a codificada en una variable especial: errno (declarada en errno.h). Puedes comparar su valor con el de las constantes predefinidas en errno.h para averiguar qu´e error concreto se ha cometido: EACCESS: permiso denegado, EEXIST: el fichero no existe, EMFILE: demasiados ficheros abiertos, . . . Introducci´on a la Programaci´on con C 353
  • 360. 5.4 Errores Truncamiento de ficheros Las funciones est´andar de manejo de ficheros no permiten efectuar una operaci´on que puede resultar necesaria en algunas aplicaciones: eliminar elementos de un fichero. Una forma de conseguir este efecto consiste en generar un nuevo fichero en el que escribimos s´olo aquellos elementos que no deseamos eliminar. Una vez generado el nuevo fichero, borramos el original y renombramos el nuevo para que adopte el nombre del original. Costoso. En Unix puedes recurrir a la funci´on truncate (disponible al incluir la cabecera unistd.h). El perfil de truncate es ´este: int truncate(char nombre[], int longitud); La funci´on recibe el nombre de un fichero (que no debe estar abierto) y el n´umero de bytes que deseamos conservar. Si la llamada tiene ´exito, la funci´on hace que en el fichero s´olo permanezcan los longitud primeros bytes y devuelve el valor 0. En caso contrario, devuelve el valor −1. Observa que s´olo puedes borrar los ´ultimos elementos de un fichero, y no cualquiera de ellos. Por eso la acci´on de borrar parte de un fichero recibe el nombre de truncamiento. Como manejarte con tantas constantes (algunas con significados un tanto dif´ıcil de com- prender hasta que curses asignaturas de sistemas operativos) resulta complicado, puedes usar una funci´on especial: void perror (char s[]); Esta funci´on muestra por pantalla el valor de la cadena s, dos puntos y un mensaje de error que detalla la causa del error cometido. La cadena s, que suministra el programador, suele indicar el nombre de la funci´on en la que se detect´o el error, ayudando as´ı a la depuraci´on del programa. 354 Introducci´on a la Programaci´on con C
  • 361. Ap´endice A Tipos b´asicos A.1. Enteros A.1.1. Tipos Esta tabla muestra el nombre de cada uno de los tipos de datos para valores enteros (algunos tienen dos nombres v´alidos), su rango de representaci´on y el n´umero de bytes (grupos de 8 bits) que ocupan. Tipo Rango Bytes char −128 . . . 127 1 short int (o short) −32768 . . . 32767 2 int −2147483648 . . . 2147483647 4 long int (o long) −2147483648 . . . 2147483647 4 long long int (o long long) −9223372036854775808 . . . 9223372036854775807 8 (Como ves, los tipos short int, long int y long long int pueden abreviarse, respectivamente, como short, long, y long long.) Un par de curiosidades sobre la tabla de tipos enteros: Los tipos int y long int ocupan lo mismo (4 bytes) y tienen el mismo rango. Esto es as´ı para el compilador gcc sobre un PC. En una m´aquina distinta o con otro compilador, podr´ıan ser diferentes: los int podr´ıan ocupar 4 bytes y los long int, 8, por ejemplo. En sistemas m´as antiguos un int ocupaba 2 bytes y un long int, 4. El nombre del tipo char es abreviatura de ((car´acter)) (((character)), en ingl´es) y, sin em- bargo, hace referencia a los enteros de 8 bits, es decir, 1 byte. Los valores de tipo char son ambivalentes: son tanto n´umeros enteros como caracteres. Es posible trabajar con enteros sin signo en C, es decir, n´umeros enteros positivos. La ventaja de trabajar con ellos es que se puede aprovechar el bit de signo para aumentar el rango positivo y duplicarlo. Los tipos enteros sin signo tienen el mismo nombre que sus correspondientes tipos con signo, pero precedidos por la palabra unsigned, que act´ua como un adjetivo: Tipo Rango Bytes unsigned char 0. . . 255 1 unsigned short int (o unsigned short) 0. . . 65535 2 unsigned int (o unsigned) 0. . . 4294967295 4 unsigned long int (o unsigned long) 0. . . 4294967295 4 unsigned long long int (o unsigned long long) 0. . . 18446744073709551615 8 Del mismo modo que podemos ((marcar)) un tipo entero como ((sin signo)) con el adjetivo unsigned, podemos hacer expl´ıcito que tiene signo con el adjetivo signed. O sea, el tipo int puede escribirse tambi´en como signed int: son exactamente el mismo tipo, s´olo que en el segundo caso se pone ´enfasis en que tiene signo, haciendo posible una mejora en la legibilidad de un programa donde este rasgo sea importante. Introducci´on a la Programaci´on con C 355
  • 362. A.2 Flotantes A.1.2. Literales Puedes escribir n´umeros enteros en notaci´on octal (base 8) o hexadecimal (base 16). Un n´umero en notaci´on hexadecimal empieza por 0x. Por ejemplo, 0xff es 255 y 0x0 es 0. Un n´umero en notaci´on octal debe empezar por un 0 y no ir seguido de una x. Por ejemplo, 077 es 63 y 010 es 8.1 Puedes precisar que un n´umero entero es largo a˜nadi´endole el sufijo L (por ((Long))). Por ejemplo, 2L es el valor 2 codificado con 32 bits. El sufijo LL (por ((long long))) indica que el n´umero es un long long int. El literal 2LL, por ejemplo, representa al n´umero entero 2 codificado con 64 bits (lo que ocupa un long long int). El sufijo U (combinado opcionalmente con L o LL) precisa que un n´umero no tiene signo (la U por ((unsigned))). Normalmente no necesitar´as usar esos sufijos, pues C hace conversiones autom´aticas de tipo cuando conviene. S´ı te har´a falta si quieres denotar un n´umero mayor que 2147483647 (o menor que −2147483648), pues en tal caso el n´umero no puede representarse como un simple int. Por ejemplo, la forma correcta de referirse a 3000000000 es con el literal 3000000000LL. C resulta abrumador por la gran cantidad de posibilidades que ofrece. Son muchas formas diferentes de representar enteros, ¿verdad? No te preocupes, s´olo en aplicaciones muy concretas necesitar´as utilizar la notaci´on octal o hexadecimal o tendr´as que a˜nadir el sufijo a un literal para indicar su tipo. A.1.3. Marcas de formato Hay una marca de formato para la impresi´on o lectura de valores de cada tipo de entero: Tipo Marca Tipo Marca char (n´umero) %hhd unsigned char %hhu short %hd unsigned short %hu int %d unsigned %u long %ld unsigned long %lu long long %lld unsigned long long %llu Puedes mostrar los valores num´ericos en base octal o hexadecimal sustituyendo la d (o la u) por una o o una x, respectivamente. Por ejemplo, %lx es una marca que muestra un entero largo en hexadecimal y %ho muestra un short en octal. Son muchas, ¿verdad? La que usar´as m´as frecuentemente es %d. De todos modos, por si necesitas utilizar otras, he aqu´ı algunas reglas mnemot´ecnicas: d significa ((decimal)) y alude a la base en que se representa la informaci´on: base 10. Por otra parte, x y o representan a ((hexadecimal)) y ((octal)) y aluden a las bases 16 y 8. u significa ((unsigned)), es decir, ((sin signo)). h significa ((mitad)) (por ((half))), as´ı que %hd es ((la mitad)) de un entero, o sea, un short, y %hhd es ((la mitad de la mitad)) de un entero, o sea, un char. l significa ((largo)) (por ((long))), as´ı que %ld es un entero largo (un long) y %lld es un entero extra-largo (un long long). A.2. Flotantes A.2.1. Tipos Tambi´en en el caso de los flotantes tenemos d´onde elegir: hay tres tipos diferentes. En esta tabla te mostramos el nombre, m´aximo valor absoluto y n´umero de bits de cada uno de ellos: Tipo M´aximo valor absoluto Bytes float 3.40282347·1038 4 double 1.7976931348623157·10308 8 long double 1.189731495357231765021263853031·104932 12 1Lo cierto es que tambi´en puede usar notaci´on octal o hexadecimal en Python, aunque en su momento no lo contamos. 356 Introducci´on a la Programaci´on con C
  • 363. CC 2003, 2008 Andr´es Marzal e Isabel Gracia A Tipos b´asicos Recuerda que los n´umeros expresados en coma flotante presentan mayor resoluci´on en la cercan´ıas del 0, y que ´esta es tanto menor cuanto mayor es, en valor absoluto, el n´umero representado. El n´umero no nulo m´as pr´oximo a cero que puede representarse con cada uno de los tipos se muestra en esta tabla: Tipo M´ınimo valor absoluto no nulo float 1.17549435·10−38 double 2.2250738585072014·10−308 long double 3.3621031431120935062626778173218·10−4932 A.2.2. Literales Ya conoces las reglas para formar literales para valores de tipo float. Puedes a˜nadir el sufijo F para precisar que el literal corresponde a un double y el sufijo L para indicar que se trata de un long double. Por ejemplo, el literal 3.2F es el valor 3.2 codificado como double. Al igual que con los enteros, normalmente no necesitar´as precisar el tipo del literal con el sufijo L, a menos que su valor exceda del rango propio de los float. A.2.3. Marcas de formato Veamos ahora las principales marcas de formato para la impresi´on de datos de tipos flo- tantes: Tipo Notaci´on convencional Notaci´on cient´ıfica float %f %e double %f %e long double %Lf %Le Observa que tanto float como double usan la misma marca de formato para impresi´on (o sea, con la funci´on printf y similares). No pretendemos detallar todas las marcas de formato para flotantes. Tenemos, adem´as, otras como %E, %F, %g, %G, %LE, %LF, %Lg y %LG. Cada marca introduce ciertos matices que, en seg´un qu´e aplicaciones, pueden venir muy bien. Necesitar´as un buen manual de referencia a mano para controlar estos y otros muchos aspectos (no tiene sentido memorizarlos) cuando ejerzas de programador en C durante tu vida profesional.2 Las marcas de formato para la lectura de datos de tipos flotantes presentan alguna diferencia: Tipo Notaci´on convencional float %f double %lf long double %Lf Observa que la marca de impresi´on de un double es %f, pero la de lectura es %lf. Es una incoherencia de C que puede darte alg´un que otro problema. A.3. Caracteres El tipo char, que ya hemos presentado al estudiar los tipos enteros, es, a la vez el tipo con el que solemos representar caracteres y con el que formamos las cadenas. A.3.1. Literales Los literales de car´acter encierran entre comillas simples al car´acter en cuesti´on o lo codifican como un n´umero entero. Es posible utilizar secuencias de escape para indicar el car´acter que se encierra entre comillas. 2En Unix puedes obtener ayuda acerca de las funciones est´andar con el manual en l´ınea. Ejecuta man 3 printf, por ejemplo, y obtendr´as una p´agina de manual sobre la funci´on printf , incluyendo informaci´on sobre todas sus marcas de formato y modificadores. Introducci´on a la Programaci´on con C 357
  • 364. A.4 Otros tipos b´asicos A.3.2. Marcas de formato Los valores de tipo char pueden mostrarse en pantalla (o escribirse en ficheros de texto) usando la marca %c o %hhd. La primera marca muestra el car´acter como eso mismo, como car´acter; la segunda muestra su valor decimal (el c´odigo ASCII del car´acter). A.4. Otros tipos b´asicos C99 define tres nuevos tipos b´asicos: el tipo l´ogico (o booleano), el tipo complejo y el tipo imaginario. A.4.1. El tipo booleano Las variables de tipo _Bool pueden almacenar los valores 0 (((falso))) o 1 (((cierto))). Si se incluye la cabecera <stdbool.h> es posible usar el identificador de tipo bool y las constantes true y false para referirse al tipo _Bool y a los valores 1 y 0, respectivamente. A.4.2. Los tipos complejo e imaginario C99 ofrece soporte para la aritm´etica compleja a trav´es de los tipos _Complex e _Imaginary. A.5. Una reflexi´on acerca de la diversidad de tipos escalares ¿Por qu´e ofrece C tan gran variedad de tipos de datos para enteros y flotantes? Porque C procura facilitar el dise˜no de programas eficientes proporcionando al programador un juego de tipos que le permita adoptar el compromiso adecuado entre ocupaci´on de memoria y rango disponible. ¿Por qu´e iba un programador a querer gastar 4 bytes en una variable que s´olo almacenar´a valores entre 0 y 255? Naturalmente, ofrecer m´as control no es gratis: a cambio hemos de tomar muchas m´as decisiones. Ahorrar 3 bytes en una variable puede no justificar el quebradero de cabeza, pero piensa en el ahorro que se puede producir en un vector que contiene miles o cientos de miles de elementos que pueden representarse cada uno con un char en lugar de con un int. Por otra parte, la arquitectura de tu ordenador est´a optimizada para realizar c´alculos con valores de ciertos tama˜nos. Por ejemplo, las operaciones con enteros suelen ser m´as r´apidas si trabajas con int (aunque ocupen m´as bytes que los char o short) y las operaciones con flotantes m´as eficientes trabajan con double. Seg´un si valoras m´as velocidad o consumo de memoria en una aplicaci´on, deber´as escoger uno u otro tipo de datos para ciertas variables. 358 Introducci´on a la Programaci´on con C
  • 365. Ap´endice B La lectura de datos por teclado, paso a paso B.1. La lectura de valores escalares con scanf La funci´on scanf (y fscanf ) se comporta de un modo un tanto especial y puede desconcertarte en ocasiones. Veamos qu´e hace exactamente scanf : Empieza salt´andose los blancos que encuentra (espacios en blanco, tabuladores y saltos de l´ınea). A continuaci´on, ((consume)) los caracteres no blancos mientra ((le sirvan)) para leer un valor del tipo que se indica con la marca de formato (por ejemplo, d´ıgitos si la marca es %d). La lectura se detiene cuando el siguiente car´acter a leer ((no sirve)) (por ejemplo, una letra si estamos leyendo un entero). Dicho car´acter no es ((consumido)). Los caracteres ((consumidos)) hasta este punto se interpretan como la representaci´on de un valor del tipo que se indica con la correspondiente marca de formato, as´ı que se crea dicho valor y se escribe en la zona de memoria que empieza en la direcci´on que se indique. Un ejemplo ayudar´a a entender el comportamiento de scanf : lee tres.c lee tres.c 1 #include <stdio.h> 2 3 int main(void) 4 { 5 int a, c; 6 float b; 7 8 printf ("Entero a: "); scanf ("%d", &a); 9 printf ("Flotante b: "); scanf ("%f", &b); 10 printf ("Entero c: "); scanf ("%d", &c); 11 printf ("El entero a es %d, el flotante b es %f y el entero c es %d 12 ", a, b, c); 13 14 return 0; 15 } Ejecutemos el programa e introduzcamos los valores 20, 3.0 y 4 pulsando el retorno de carro tras cada uno de ellos. Entero a: 20 Flotante b: 3.0 Entero c: 4 El entero a es 20, el flotante b es 3.000000 y el entero c es 4 Perfecto. Para ver qu´e ha ocurrido paso a paso vamos a representar el texto que escribe el usuario durante la ejecuci´on como una secuencia de teclas. En este gr´afico se muestra qu´e Introducci´on a la Programaci´on con C 359
  • 366. B.1 La lectura de valores escalares con scanf ocurre durante la ejecuci´on del primer scanf (l´ınea 8), momento en el que las tres variables est´an sin inicializar y el usuario acaba de pulsar las teclas 2, 0 y retorno de carro: 2 0 n a b c El car´acter a la derecha de la flecha es el siguiente car´acter que va a ser consumido. La ejecuci´on del primer scanf consume los caracteres ’2’ y ’0’, pues ambos son v´alidos para formar un entero. La funci´on detecta el blanco (salto de l´ınea) que sigue al car´acter ’0’ y se detiene. Interpreta entonces los caracteres que ha le´ıdo como el valor entero 20 y lo almacena en la direcci´on de memoria que se la suministrado (&a): 2 0 n &a 20a b c En la figura hemos representado los caracteres consumidos en color gris. F´ıjate en que el salto de l´ınea a´un no ha sido consumido. La ejecuci´on del segundo scanf , el que lee el contenido de b, empieza descartando los blancos iniciales, es decir, el salto de l´ınea: 2 0 n 20a b c Como no hay m´as caracteres que procesar, scanf queda a la espera de que el usuario teclee algo con lo que pueda formar un flotante y pulse retorno de carro. Cuando el usaurio teclea el 3.0 seguido del salto de l´ınea, pasamos a esta nueva situaci´on: 2 0 n 3 . 0 n 20a b c Ahora, scanf reanuda su ejecuci´on y consume el ’3’, el ’.’ y el ’0’. Como detecta que lo que sigue no es v´alido para formar un flotante, se detiene, interpreta los caracteres le´ıdos como el valor flotante 3.0 y lo almacena en la direcci´on de b: 2 0 n 3 . 0 n 20a &b 3.0b c Finalmente, el tercer scanf entra en ejecuci´on y empieza por saltarse el salto de l´ınea. 2 0 n 3 . 0 n 20a 3.0b c Acto seguido se detiene, pues no es necesario que el usuario introduzca nuevo texto que procesar. Entonces el usuario escribe el 4 y pulsa retorno: 360 Introducci´on a la Programaci´on con C
  • 367. CC 2003, 2008 Andr´es Marzal e Isabel Gracia B La lectura de datos por teclado, paso a paso 2 0 n 3 . 0 n 4 n 20a 3.0b c Ahora scanf prosigue consumiendo el 4 y deteni´endose nuevamente ante el salto de l´ınea. El car´acter le´ıdo se interpreta entonces como el entero 4 y se almacena en la direcci´on de memoria de c: 2 0 n 3 . 0 n 4 n 20a 3.0b &c 4c Como puedes apreciar, el ´ultimo salto de l´ınea no llega a ser consumido, pero eso importa poco, pues el programa finaliza correctamente su ejecuci´on. Vamos a estudiar ahora el porqu´e de un efecto curioso. Imagina que, cuando el programa pide al usuario el primer valor entero, ´este introduce tanto dicho valor como los dos siguientes, separando los tres valores con espacios en blanco. He aqu´ı el resultado en pantalla: Entero a: 20 3.0 4 Flotante b: Entero c: El entero a es 20, el flotante b es 3.000000 y el entero c es 4 El programa ha le´ıdo correctamente los tres valores, sin esperar a que el usuario introduzca tres l´ıneas con datos: cuando ten´ıa que detenerse para leer el valor de b, no lo ha hecho, pues ((sab´ıa)) que ese valor era 3.0; y tampoco se ha detenido al leer el valor de c, ya que de alg´un modo ((sab´ıa)) que era 4. Veamos paso a paso lo que ha sucedido, pues la explicaci´on es bien sencilla. Durante la ejecuci´on del primer scanf , el usuario ha escrito el siguiente texto: 2 0 3 . 0 4 n a b c Como su objetivo es leer un entero, ha empezado a consumir caracteres. El ’2’ y el ’0’ le son ´ultiles, as´ı que los ha consumido. Entonces se ha detenido frente al espacio en blanco. Los caracteres le´ıdos se interpretan como el valor entero 20 y se almacenan en a: 2 0 3 . 0 4 n &a 20a b c La ejecuci´on del siguiente scanf no ha detenido la ejecuci´on del programa, pues a´un hab´ıa caracteres pendientes de procesar en la entrada. Como siempre, scanf se ha saltado el primer blanco y ha ido encontrando caracteres v´alidos para ir formando un valor del tipo que se le indica (en este caso, un flotante). La funci´on scanf ha dejado de consumir caracteres al encontrar un nuevo blanco, se ha detenido y ha almacenado en b el valor flotante 3.0. He aqu´ı el nuevo estado: 2 0 3 . 0 4 n 20a &b 3.0b c Introducci´on a la Programaci´on con C 361
  • 368. B.2 La lectura de cadenas con scanf Finalmente, el tercer scanf tampoco ha esperado nueva entrada de teclado: se ha saltado direc- tamente el siguiente blanco, ha encontrado el car´acter ’4’ y se ha detenido porque el car´acter n que le sigue es un blanco. El valor le´ıdo (el entero 4) se almacena en c: 2 0 3 . 0 4 n 20a 3.0b &c 4c Tras almacenar en c el entero 4, el estado es ´este: 2 0 3 . 0 4 n 20a 3.0b 4c Cuando observes un comportamiento inesperado de scanf , haz un an´alisis de lo sucedido como el que te hemos presentado aqu´ı y ver´as que todo tiene explicaci´on. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ejercicios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . · 338 ¿Qu´e pasa si el usuario escribe la siguiente secuencia de caracteres como datos de entrada en la ejecuci´on del programa? 2 0 3 . 1 2 3 4 5 5 n · 339 ¿Qu´e pasa si el usuario escribe la siguiente secuencia de caracteres como datos de entrada en la ejecuci´on del programa? 2 0 3 . 1 2 3 n 4 5 5 n · 340 ¿Qu´e pasa si el usuario escribe la siguiente secuencia de caracteres como datos de entrada en la ejecuci´on del programa? 2 0 2 4 5 x n · 341 ¿Qu´e pasa si el usuario escribe la siguiente secuencia de caracteres como datos de entrada en la ejecuci´on del programa? 6 x 2 n (Prueba este ejercicio con el ordenador.) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . B.2. La lectura de cadenas con scanf Vamos a estudiar ahora el comportamiento paso a paso de scanf cuando leemos una cadena: Se descartan los blancos iniciales (espacios en blanco, tabuladores o saltos de l´ınea). Se leen los caracteres ((v´alidos)) hasta el primer blanco y se almacenan en posiciones de memoria consecutivas a partir de la que se suministra como argumento. Se entiende por car´acter v´alido cualquier car´acter no blanco (ni tabulador, ni espacio en blanco, ni salto de l´ınea. . . ). Se a˜nade al final un terminador de cadena. Un ejemplo ayudar´a a entender qu´e ocurre ante ciertas entradas: 362 Introducci´on a la Programaci´on con C
  • 369. CC 2003, 2008 Andr´es Marzal e Isabel Gracia B La lectura de datos por teclado, paso a paso lee cadena.c lee cadena.c 1 #include <stdio.h> 2 3 #define TALLA 10 4 5 int main(void) 6 { 7 char a[TALLA+1], b[TALLA+1]; 8 9 printf ("Cadena 1: "); scanf ("%s", a); 10 printf ("Cadena 2: "); scanf ("%s", b); 11 printf ("La cadena 1 es %s y la cadena 2 es %sn", a, b); 12 13 return 0; 14 } Si ejecutas el programa y escribes una primera cadena sin blancos, pulsas el retorno de carro, escribes otra cadena sin blancos y vuelves a pulsar el retorno, la lectura se efect´ua como cabe esperar: Cadena 1: uno Cadena 2: dos La cadena 1 es uno y la cadena 2 es dos Estudiemos paso a paso lo ocurrido. Ante el primer scanf , el usuario ha escrito lo siguiente: u n o n La funci´on ha empezado a consumir los caracteres con los que ir formando la cadena. Al llegar al salto de l´ınea se ha detenido sin consumirlo. He aqu´ı el nuevo estado de cosas: u n o n a u 0 n 1 o 2 0 3 4 5 6 7 8 9 (F´ıjate en que scanf termina correctamente la cadena almacenada en a.) Acto seguido se ha ejecutado el segundo scanf . La funci´on se salta entonces el blanco inicial, es decir, el salto de l´ınea que a´un no hab´ıa sido consumido. u n o n a u 0 n 1 o 2 0 3 4 5 6 7 8 9 Como no hay m´as caracteres, scanf ha detenido la ejecuci´on a la espera de que el usuario teclee algo. Entonces el usuario ha escrito la palabra dos y ha pulsado retorno de carro: u n o n d o s n a u 0 n 1 o 2 0 3 4 5 6 7 8 9 Entonces scanf ha procedido a consumir los tres primeros caracteres: Introducci´on a la Programaci´on con C 363
  • 370. B.2 La lectura de cadenas con scanf u n o n d o s n a u 0 n 1 o 2 0 3 4 5 6 7 8 9 b d 0 o 1 s 2 0 3 4 5 6 7 8 9 F´ıjate en que scanf introduce autom´aticamente el terminador pertinente al final de la cadena le´ıda. El segundo scanf nos conduce a esta nueva situaci´on: u n o n d o s n a u 0 n 1 o 2 0 3 4 5 6 7 8 9 b d 0 o 1 s 2 0 3 4 5 6 7 8 9 Compliquemos un poco la situaci´on. ¿Qu´e ocurre si, al introducir las cadenas, metemos espacios en blanco delante y detr´as de las palabras? Cadena 1: uno Cadena 2: dos La cadena 1 es uno y la cadena 2 es dos Recuerda que scanf se salta siempre los blancos que encuentra al principio y que se detiene en el primer espacio que encuentra tras empezar a consumir caracteres v´alidos. Ve´amoslo paso a paso. Empezamos con este estado de la entrada: u n o n El primer scanf empieza salt´andose los blancos inciales: u n o n A continuaci´on consume los caracteres ’u’, ’n’ y ’o’ y se detiene al detectar el blanco que sigue: u n o n a u 0 n 1 o 2 0 3 4 5 6 7 8 9 Cuando se ejecuta, el segundo scanf empieza salt´andose los blancos iniciales, que son todos los que hay hasta el salto de l´ınea (inclu´ıdo ´este): u n o n a u 0 n 1 o 2 0 3 4 5 6 7 8 9 De nuevo, como no hay m´as que leer, la ejecuci´on se detiene. El usuario teclea entonces nuevos caracteres: 364 Introducci´on a la Programaci´on con C
  • 371. CC 2003, 2008 Andr´es Marzal e Isabel Gracia B La lectura de datos por teclado, paso a paso u n o n d o s n a u 0 n 1 o 2 0 3 4 5 6 7 8 9 A continuaci´on, sigue salt´andose los blancos: u n o n d o s n a u 0 n 1 o 2 0 3 4 5 6 7 8 9 Pasa entonces a consumir caracteres no blancos y se detiene ante el primer blanco: u n o n d o s n a u 0 n 1 o 2 0 3 4 5 6 7 8 9 b d 0 o 1 s 2 0 3 4 5 6 7 8 9 Ya est´a. Imagina ahora que nuestro usuario quiere introducir en a la cadena "uno dos" y en b la cadena "tres". Aqu´ı tienes lo que ocurre al ejecutar el programa Cadena 1: uno dos Cadena 2: La cadena 1 es uno y la cadena 2 es dos El programa ha finalizado sin darle tiempo al usuario a introducir la cadena "tres". Es m´as, la primera cadena vale "uno" y la segunda "dos", con lo que ni siquiera se ha conseguido el primer objetivo: leer la cadena "uno dos" y depositarla tal cual en a. Analicemos paso a paso lo sucedido. La entrada que el usuario teclea ante el primer scanf es ´esta: u n o d o s n La funci´on lee en a los caracteres ’u’, ’n’ y ’o’ y se detiene al detectar un blanco. El nuevo estado se puede representar as´ı: u n o d o s n a u 0 n 1 o 2 0 3 4 5 6 7 8 9 El segundo scanf entra en juego entonces y ((aprovecha)) lo que a´un no ha sido procesado, as´ı que empieza por descartar el blanco inicial y, a continuaci´on, consume los caracteres ’d’, ’o’, ’s’: u n o d o s n a u 0 n 1 o 2 0 3 4 5 6 7 8 9 b d 0 o 1 s 2 0 3 4 5 6 7 8 9 Introducci´on a la Programaci´on con C 365
  • 372. B.3 Un problema serio: la lectura alterna de cadenas con gets y de escalares con scanf ¿Ves? La consecuencia de este comportamiento es que con scanf s´olo podemos leer palabras individuales. Para leer una l´ınea completa en una cadena, hemos de utilizar una funci´on distinta: gets (por ((get string)), que en ingl´es significa ((obt´en cadena))), disponible incluyendo stdio.h en nuestro programa. B.3. Un problema serio: la lectura alterna de cadenas con gets y de escalares con scanf Vamos a estudiar un caso concreto y analizaremos las causas del extra˜no comportamiento observado. lee alterno mal.c lee alterno mal.c 1 #include <stdio.h> 2 3 #define TALLA 80 4 5 int main(void) 6 { 7 char a[TALLA+1], b[TALLA+1]; 8 int i; 9 10 printf ("Cadena a: "); gets(a); 11 printf ("Entero i: "); scanf ("%d", &i); 12 printf ("Cadena b: "); gets(b); 13 printf ("La cadena a es %s, el entero i es %d y la cadena b es %sn", a, i, b); 14 15 return 0; 16 } Observa que leemos cadenas con gets y un entero con scanf . Vamos a ejecutar el programa introduciendo la palabra uno en la primera cadena, el valor 2 en el entero y la palabra dos en la segunda cadena. Cadena a: uno Entero i: 2 Cadena b: La cadena a es uno, el entero i es 2 y la cadena b es ¿Qu´e ha pasado? No hemos podido introducir la segunda cadena: ¡tan pronto hemos escrito el retorno de carro que sigue al 2, el programa ha finalizado! Estudiemos paso a paso lo ocurrido. El texto introducido ante el primer scanf es: u n o n El primer gets nos deja en esta situaci´on: u n o n a u 0 n 1 o 2 0 3 4 5 6 7 8 9 A continuaci´on se ejecuta el scanf con el que se lee el valor de i. El usuario teclea lo siguiente: u n o n 2 n a u 0 n 1 o 2 0 3 4 5 6 7 8 9 2i 366 Introducci´on a la Programaci´on con C
  • 373. CC 2003, 2008 Andr´es Marzal e Isabel Gracia B La lectura de datos por teclado, paso a paso La funci´on lee el 2 y encuentra un salto de l´ınea. El estado en el que queda el programa es ´este: u n o n 2 n a u 0 n 1 o 2 0 3 4 5 6 7 8 9 2i F´ıjate bien en qu´e ha ocurrido: nos hemos quedado a las puertas de procesar el salto de l´ınea. Cuando el programa pasa a ejecutar el siguiente gets, ¡lee una cadena vac´ıa! ¿Por qu´e? Porque gets lee caracteres hasta el primer salto de l´ınea, y el primer car´acter con que nos encontramos ya es un salto de l´ınea. Pasamos, pues, a este nuevo estado: u n o n 2 n a u 0 n 1 o 2 0 3 4 5 6 7 8 9 2i b 0 0 1 2 3 4 5 6 7 8 9 ¿C´omo podemos evitar este problema? Una soluci´on posible consiste en consumir la cadena vac´ıa con un gets extra y una variable auxiliar. F´ıjate en este programa: lee alterno bien.c lee alterno bien.c 1 #include <stdio.h> 2 3 #define TALLA 80 4 5 int main(void) 6 { 7 char a[TALLA+1], b[TALLA+1]; 8 int i; 9 char findelinea[TALLA+1]; // Cadena auxiliar. Su contenido no nos importa. 10 11 printf ("Cadena a: "); gets(a); 12 printf ("Entero i: "); scanf ("%d", &i); gets(findelinea); 13 printf ("Cadena b: "); gets(b); 14 printf ("La cadena a es %s, el entero i es %d y la cadena b es %sn", a, i, b); 15 16 return 0; 17 } Hemos introducido una variable extra, findelinea, cuyo ´unico objetivo es consumir lo que scanf no ha consumido. Gracias a ella, ´este es el estado en que nos encontramos justo antes de empezar la lectura de b: u n o n 2 n a u 0 n 1 o 2 0 3 4 5 6 7 8 9 2i findelinea 0 0 1 2 3 4 5 6 7 8 9 Introducci´on a la Programaci´on con C 367
  • 374. B.3 Un problema serio: la lectura alterna de cadenas con gets y de escalares con scanf El usuario escribe entonces el texto que desea almacenar en b: u n o n 2 n d o s n a u 0 n 1 o 2 0 3 4 5 6 7 8 9 2i findelinea 0 0 1 2 3 4 5 6 7 8 9 Ahora la lectura de b tiene ´exito. Tras ejecutar gets, ´este es el estado resultante: u n o n 2 n d o s n a u 0 n 1 o 2 0 3 4 5 6 7 8 9 2i findelinea 0 0 1 2 3 4 5 6 7 8 9 b d 0 o 1 s 2 0 3 4 5 6 7 8 9 ¡Perfecto! Ya te dijimos que aprender C iba a suponer enfrentarse a algunas dificultades de car´acter t´ecnico. La ´unica forma de superarlas es conocer bien qu´e ocurre en las entra˜nas del programa. Pese a que esta soluci´on funciona, facilita la comisi´on de errores. Hemos de recordar consumir el fin de l´ınea s´olo en ciertos contexto. Esta otra soluci´on es m´as sistem´atica: leer siempre l´ınea a l´ınea con gets y, cuando hay de leerse un dato entero, flotante, etc., hacerlo con sscanf sobre la cadena le´ıda: lee alterno bien.c lee alterno bien.c 1 #include <stdio.h> 2 3 #define TALLA 80 4 5 int main(void) 6 { 7 char a[TALLA+1], b[TALLA+1]; 8 int i; 9 char linea[TALLA+1]; // Cadena auxiliar. Su contenido no nos importa. 10 11 printf ("Cadena a: "); gets(a); 12 printf ("Entero i: "); gets(linea); sscanf (linea , "%d", &i); 13 printf ("Cadena b: "); gets(b); 14 printf ("La cadena a es %s, el entero i es %d y la cadena b es %sn", a, i, b); 15 16 return 0; 17 } 368 Introducci´on a la Programaci´on con C
  • 375. CC 2003, 2008 Andr´es Marzal e Isabel Gracia B La lectura de datos por teclado, paso a paso ((¡Ah, ya s´e!, ¡es un libro del Espejo, naturalmente! Si lo pongo delante de un espejo, las palabras se ver´an otra vez al derecho.)) Y ´este es el poema que ley´o Alicia11 : JERIG ´ONDOR Cocillaba el d´ıa y las tovas agilimosas giroscopaban y barrenaban en el larde. Todos debirables estaban los burgovos, y silbramaban las alecas rastas. 11. [...] Carroll pasa a continuaci´on a interpretar las palabras de la manera siguiente: Bryllig [“cocillaba”] (der. del verbo “Bryl” o “Broil”); “hora de cocinar la comida; es decir, cerca de la hora de comer”. Slythy [“agilimosas”] (voz compuesta por “Slimy” y “Lithe”. “Suave y activo”. Tova. Especie de tej´on. Ten´ıa suave pelo blanco, largas patas traseras y cuernos cortos como de ciervo, se alimentaba principalmente de queso. Gyre [“giroscopar”], verbo (derivado de Gyaour o Giaour, “perro”). “Ara˜nar como un perro”. Gymble [“barrenar”], (de donde viene Gimblet [“barrena”]) “hacer agujeros en algo”. Wave [“larde”] (derivado del verbo “to swab” [“fregar”] o “soak” [“empapar”]). “Ladera de una colina” (del hecho de empaparse por acci´on de la lluvia). Mimsy (de donde viene Mimserable y Miserable): “infeliz”. Borogove [“burgovo”], especie extinguida de loro. Carec´ıa de alas, ten´ıa el pico hacia arriba, y anidaba bajo los relojes de sol: se alimentaba de ternera. Mome [“aleca”] (de donde viene Solemome y Solemne). Grave. Rath [“rasta”]. Especie de tortuga de tierra. Cabeza erecta, boca de tibur´on, patas anteriores torcidas, de manera que el animal caminaba sobre sus rodillas; cuerpo liso de color verde; se alimentaba de golondrinas y ostras. Outgrabe [“silbramar”]. Pret´erito del verbo Outgribe (emparentado con el antiguo to Grike o Shrike, del que proceden “Shreak” [“chillar”] y “Creak” [“chirriar”]: “chillaban”. Por tanto, el pasaje dice literalmente: “Era por la tarde, y los tejones, suaves y activos, hurgaban y hac´ıan agujeros en las laderas; los loros eran muy desdichados, y las graves tortugas profer´ıan chillidos.” Alicia anotada (Edici´on de Martin Gardner), Lewis Carroll. Introducci´on a la Programaci´on con C 369