Sunteți pe pagina 1din 373

Introducción a la programación

con C

Andrés Marzal Isabel Gracia


Departamento de Lenguajes y Sistemas Informáticos
Universitat Jaume I

c 2003 de Andrés Marzal Varó e Isabel Gracia Luengo. Reservados todos los derechos.
Esta ((Edición Internet)) se puede reproducir con fines autodidactas o para su uso en
centros públicos de enseñanza, exclusivamente. En el segundo caso, únicamente se car-
garán al estudiante los costes de reproducción. La reproducción total o parcial con ánimo
de lucro o con cualquier finalidad comercial está estrictamente prohibida sin el permiso
escrito de los autores.
Índice general

1. Introducción a C 1
1.1. C es un lenguaje compilado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2. Traduciendo de Python a C: una guı́a rápida . . . . . . . . . . . . . . . . . . . . 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 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
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 . . . . . . . . . . . . . . 25
1.8.1. Identificadores válidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.8.2. Sentencias de declaración . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.8.3. Declaración con inicialización . . . . . . . . . . . . . . . . . . . . . . . . . 26
1.9. Salida por pantalla . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
1.9.1. Marcas de formato para la impresión de valores con printf . . . . . . . . . 27
1.10. Variables y direcciones de memoria . . . . . . . . . . . . . . . . . . . . . . . . . . 31
1.11. Entrada por teclado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
1.12. Expresiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
1.13. Conversión implı́cita y explı́cita de tipos . . . . . . . . . . . . . . . . . . . . . . . 41
1.14. Las directivas y el preprocesador . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
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 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
1.16. Las bibliotecas (módulos) se importan con #include . . . . . . . . . . . . . . . . 47
1.16.1. La biblioteca matemática . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
1.17. Estructuras de control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
1.17.1. Estructuras de control condicionales . . . . . . . . . . . . . . . . . . . . . 49
1.17.2. Estructuras de control iterativas . . . . . . . . . . . . . . . . . . . . . . . 53
1.17.3. Sentencias para alterar el flujo iterativo . . . . . . . . . . . . . . . . . . . 59

2. Estructuras de datos en C: vectores estáticos y registros 61


2.1. Vectores estáticos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
2.1.1. Declaración de vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
2.1.2. Inicialización de los vectores . . . . . . . . . . . . . . . . . . . . . . . . . . 62
2.1.3. Un programa de ejemplo: la criba de Eratóstenes . . . . . . . . . . . . . . 63
2.1.4. Otro programa de ejemplo: estadı́sticas . . . . . . . . . . . . . . . . . . . 66
2.1.5. Otro programa de ejemplo: una calculadora para polinomios . . . . . . . . 75
2.1.6. Disposición de los vectores en memoria . . . . . . . . . . . . . . . . . . . . 81
2.1.7. Algunos problemas de C: accesos ilı́citos a memoria . . . . . . . . . . . . 85

Introducción a la Programación con C i


ÍNDICE GENERAL 2004/02/10-16:33

2.1.8. Asignación y copia de vectores . . . . . . . . . . . . . . . . . . . . . . . . 86


2.1.9. Comparación de vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
2.2. Cadenas estáticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
2.2.1. Declaración de cadenas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
2.2.2. Representación de las cadenas en memoria . . . . . . . . . . . . . . . . . . 89
2.2.3. Entrada/salida de cadenas . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
2.2.4. Asignación y copia de cadenas . . . . . . . . . . . . . . . . . . . . . . . . 95
2.2.5. Longitud de una cadena . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
2.2.6. Concatenación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
2.2.7. Comparación de cadenas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
2.2.8. Funciones útiles para manejar caracteres . . . . . . . . . . . . . . . . . . . 105
2.2.9. Escritura en cadenas: sprintf . . . . . . . . . . . . . . . . . . . . . . . . . 106
2.2.10. Un programa de ejemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
2.3. Vectores multidimensionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
2.3.1. Sobre la disposición de los vectores multidimensionales en memoria . . . . 108
2.3.2. Un ejemplo: cálculo matricial . . . . . . . . . . . . . . . . . . . . . . . . . 109
2.3.3. Vectores de cadenas, matrices de caracteres . . . . . . . . . . . . . . . . . 115
2.4. Registros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
2.4.1. Un ejemplo: registros para almacenar vectores de talla variable (pero
acotada) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
2.4.2. Un ejemplo: rectas de regresión para una serie de puntos en el plano . . . 127
2.4.3. Otro ejemplo: gestión de una coleción de CDs . . . . . . . . . . . . . . . . 130
2.5. Definición de nuevos tipos de datos . . . . . . . . . . . . . . . . . . . . . . . . . . 134

3. Funciones 137
3.1. Definición de funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
3.2. Variables locales y globales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
3.2.1. Variables locales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
3.2.2. Variables globales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
3.3. Funciones sin parámetros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
3.4. Procedimientos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
3.5. Paso de parámetros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
3.5.1. Parámetros escalares: paso por valor . . . . . . . . . . . . . . . . . . . . . 147
3.5.2. Organización de la memoria: la pila de llamadas a función . . . . . . . . . 147
3.5.3. Vectores de longitud variable . . . . . . . . . . . . . . . . . . . . . . . . . 153
3.5.4. Parámetros vectoriales: paso por referencia . . . . . . . . . . . . . . . . . 153
3.5.5. Parámetros escalares: paso por referencia mediante punteros . . . . . . . . 159
3.5.6. Paso de registros a funciones . . . . . . . . . . . . . . . . . . . . . . . . . 164
3.5.7. Paso de matrices y otros vectores multidimensionales . . . . . . . . . . . . 167
3.5.8. Tipos de retorno válidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
3.5.9. Un ejercicio práctico: miniGalaxis . . . . . . . . . . . . . . . . . . . . . . 171
3.6. Recursión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
3.6.1. Un método recursivo de ordenación: mergesort . . . . . . . . . . . . . . . 189
3.6.2. Recursión indirecta y declaración anticipada . . . . . . . . . . . . . . . . . 195
3.7. Macros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
3.8. Otras cuestiones acerca de las funciones . . . . . . . . . . . . . . . . . . . . . . . 199
3.8.1. Funciones inline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
3.8.2. Variables locales static . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200
3.8.3. Paso de funciones como parámetros . . . . . . . . . . . . . . . . . . . . . 201
3.9. Módulos, bibliotecas y unidades de compilación . . . . . . . . . . . . . . . . . . . 203
3.9.1. Declaración de prototipos en cabeceras . . . . . . . . . . . . . . . . . . . . 205
3.9.2. Declaración de variables en cabeceras . . . . . . . . . . . . . . . . . . . . 207
3.9.3. Declaración de registros en cabeceras . . . . . . . . . . . . . . . . . . . . . 208

ii Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 0 ÍNDICE GENERAL

4. Estructuras de datos: memoria dinámica 211


4.1. Vectores dinámicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
4.1.1. malloc, free y NULL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
4.1.2. Algunos ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215
4.1.3. Cadenas dinámicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
4.2. Matrices dinámicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228
4.2.1. Gestión de memoria para matrices dinámicas . . . . . . . . . . . . . . . . 228
4.2.2. Definición de un tipo ((matriz dinámica)) y de funciones para su gestión . 232
4.3. Más allá de las matrices dinámicas . . . . . . . . . . . . . . . . . . . . . . . . . . 236
4.3.1. Vectores de vectores de tallas arbitrarias . . . . . . . . . . . . . . . . . . . 236
4.3.2. Arreglos n-dimensionales . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
4.4. Redimensionamiento de la reserva de memoria . . . . . . . . . . . . . . . . . . . 241
4.4.1. Una aplicación: una agenda telefónica . . . . . . . . . . . . . . . . . . . . 249
4.5. Introducción a la gestión de registros enlazados . . . . . . . . . . . . . . . . . . . 254
4.5.1. Definición y creación de la lista . . . . . . . . . . . . . . . . . . . . . . . . 256
4.5.2. Adición de nodos (por cabeza) . . . . . . . . . . . . . . . . . . . . . . . . 257
4.5.3. Adición de un nodo (por cola) . . . . . . . . . . . . . . . . . . . . . . . . 259
4.5.4. Borrado de la cabeza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263
4.6. Listas con enlace simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
4.6.1. Creación de lista vacı́a . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
4.6.2. ¿Lista vacı́a? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
4.6.3. Inserción por cabeza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
4.6.4. Longitud de una lista . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269
4.6.5. Impresión en pantalla . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270
4.6.6. Inserción por cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271
4.6.7. Borrado de la cabeza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273
4.6.8. Borrado de la cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274
4.6.9. Búsqueda de un elemento . . . . . . . . . . . . . . . . . . . . . . . . . . . 277
4.6.10. Borrado del primer nodo con un valor determinado . . . . . . . . . . . . . 278
4.6.11. Borrado de todos los nodos con un valor dado . . . . . . . . . . . . . . . . 280
4.6.12. Inserción en una posición dada . . . . . . . . . . . . . . . . . . . . . . . . 281
4.6.13. Inserción ordenada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
4.6.14. Concatenación de dos listas . . . . . . . . . . . . . . . . . . . . . . . . . . 284
4.6.15. Borrado de la lista completa . . . . . . . . . . . . . . . . . . . . . . . . . . 285
4.6.16. Juntando las piezas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285
4.7. Listas simples con punteros a cabeza y cola . . . . . . . . . . . . . . . . . . . . . 292
4.7.1. Creación de lista vacı́a . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 293
4.7.2. Inserción de nodo en cabeza . . . . . . . . . . . . . . . . . . . . . . . . . . 293
4.7.3. Inserción de nodo en cola . . . . . . . . . . . . . . . . . . . . . . . . . . . 294
4.7.4. Borrado de la cabeza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 295
4.7.5. Borrado de la cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 296
4.8. Listas con enlace doble . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 298
4.8.1. Inserción por cabeza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 298
4.8.2. Borrado de la cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300
4.8.3. Inserción en una posición determinada . . . . . . . . . . . . . . . . . . . . 301
4.8.4. Borrado de la primera aparición de un elemento . . . . . . . . . . . . . . 303
4.9. Listas con enlace doble y puntero a cabeza y cola . . . . . . . . . . . . . . . . . . 305
4.10. Una guı́a para elegir listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 308
4.11. Una aplicación: una base de datos para discos compactos . . . . . . . . . . . . . 309
4.12. Otras estructuras de datos con registros enlazados . . . . . . . . . . . . . . . . . 316

5. Ficheros 319
5.1. Ficheros de texto y ficheros binarios . . . . . . . . . . . . . . . . . . . . . . . . . 319
5.1.1. Representación de la información en los ficheros de texto . . . . . . . . . . 319
5.1.2. Representación de la información en los ficheros binarios . . . . . . . . . . 320
5.2. Ficheros de texto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321
5.2.1. Abrir, leer/escribir, cerrar . . . . . . . . . . . . . . . . . . . . . . . . . . . 321
5.2.2. Aplicaciones: una agenda y un gestor de una colección de discos compactos328
5.2.3. Los ((ficheros)) de consola . . . . . . . . . . . . . . . . . . . . . . . . . . . 337

Introducción a la Programación con C iii


ÍNDICE GENERAL 2004/02/10-16:33

5.2.4. Un par de utilidades . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340


5.3. Ficheros binarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 342
5.3.1. Abrir, leer/escribir, cerrar . . . . . . . . . . . . . . . . . . . . . . . . . . . 342
5.3.2. Acceso directo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347
5.4. Errores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351

A. Tipos básicos 353


A.1. Enteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
A.1.1. Tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
A.1.2. Literales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 354
A.1.3. Marcas de formato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 354
A.2. Flotantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 354
A.2.1. Tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 354
A.2.2. Literales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355
A.2.3. Marcas de formato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355
A.3. Caracteres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355
A.3.1. Literales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355
A.3.2. Marcas de formato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
A.4. Otros tipos básicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
A.4.1. El tipo booleano . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
A.4.2. Los tipos complejo e imaginario . . . . . . . . . . . . . . . . . . . . . . . . 356
A.5. Una reflexión acerca de la diversidad de tipos escalares . . . . . . . . . . . . . . . 356

B. La lectura de datos por teclado, paso a paso 357


B.1. La lectura de valores escalares con scanf . . . . . . . . . . . . . . . . . . . . . . . 357
B.2. La lectura de cadenas con scanf . . . . . . . . . . . . . . . . . . . . . . . . . . . 360
B.3. Un problema serio: la lectura alterna de cadenas con gets y de escalares con scanf 364

iv Introducción a la Programación con C


Capı́tulo 1

Introducción 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ún trozo que poder leer:
((. . . Porque está todo en una lengua que no entiendo)), se dijo.
Estaba ası́:

RODN G Ó IREJ

sasomiliga savot sal y a d ı́ le aballicoC


.edral le ne nabanerrab y nabapocsorig
,sovogrub sol nabatse selbarived odoT
.satsar sacela sal nabamarblis y

Durante un rato, estuvo contemplando esto perpleja; pero al final se le ocurrió una
brillante idea. ¡Ah, ya sé!, ¡es un libro del Espejo, naturalmente! Si lo pongo delante de
un espejo, las palabras se verán otra vez del derecho.

Lewis Carroll, Alicia en el Paı́s de las Maravillas.

El lenguaje de programación C es uno de los más utilizados (si no el que más) en la programación
de sistemas software. Es similar a Python en muchos aspectos fundamentales: presenta las
mismas estructuras de control (selección condicional, iteración), 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ón como en el consumo de memoria, pero
a un precio: tenemos que proporcionar información explı́cita sobre gran cantidad de detalles,
por lo que generalmente resultan programas más 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ódulos)) (que
en C se denominan bibliotecas) y estructuras de control. Lo único 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ón de cuán diferentes son Python y C. . . y cuán semejantes. Estos dos programas, el
primero en Python y el segundo en C, calculan el valor de

b
X √
i
i=a

para sendos valores enteros de a y b introducidos por el usuario y tales que 0 ≤ a ≤ b.

Introducción a la Programación con C 1


Introducción a C 2004/02/10-16:33

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 negativo\n");
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 %d\n", 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: %f\n", 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ón a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

de vista.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 1 Compara los programas sumatorio.py y sumatorio.c. Analiza sus semejanzas y diferen-
cias. ¿Qué función desempeñan las llaves en sumatorio.c? ¿Qué función crees que desempeñan
las lı́neas 6 y 7 del programa C? ¿A qué elemento de Python se parecen las dos primeras lı́neas
de sumatorio.c? ¿Qué similitudes y diferencias aprecias entre las estructuras de control de
Python y C? ¿Cómo crees que se interpreta el bucle for del programa C? ¿Por qué algunas
lı́neas de sumatorio.c finalizan en punto y coma y otras no? ¿Qué diferencias ves entre los
comentarios Python y los comentarios C?
.............................................................................................

Un poco de historia
C ya tiene sus añitos. El nacimiento de C está estrechamente vinculado al del sistema opera-
tivo Unix. El investigador Ken Thompson, de AT&T, la compañı́a telefónica estadounidense,
se propuso diseñar un nuevo sistema operativo a principios de los setenta. Disponı́a de un
PDP-7 en el que codificó una primera versión de Unix en lenguaje ensamblador. Pronto se
impuso la conveniencia de desarrollar el sistema en un lenguaje de programación de alto
nivel, pero la escasa memoria del PDP-7 (8K de 18 bits) hizo que ideara el lenguaje de
programación B, una versión reducida de un lenguaje ya existente: BCPL. El lenguaje C
apareció como un B mejorado, fruto de las demandas impuestas por el desarrollo de Unix.
Dennis Ritchie fue el encargado del diseño del lenguaje C y de la implementación de un
compilador para él sobre un PDP-11.
C ha sufrido numerosos cambios a lo largo de su historia. La primera versión ((estable))
del lenguaje data de 1978 y se conoce como ((K&R C)), es decir, ((C de Kernighan y Ritchie)).
Esta versión fue descrita por sus autores en la primera edición del libro ((The C Programming
Language)) (un auténtico ((best-seller)) de la informática). La adopción de Unix como siste-
ma operativo de referencia en las universidades en los años 80 popularizó enormemente el
lenguaje de programación 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ón diseñados 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ón de un comité de expertos que define la
versión oficial del lenguaje. El comité ANSI X3J9 (ANSI son las siglas del American National
Standards Institute), creado en 1983, considera la inclusión de aquellas extensiones y mejo-
ras que juzga de suficiente interés para la comunidad de programadores. El 14 de diciembre
de 1989 se acordó qué era el ((C estándar)) y se publicó el documento con la especificación
en la primavera de 1990. El estándar se divulgó con la segunda edición de ((The C Pro-
gramming Language)), de Brian Kernighan y Dennis Ritchie. Un comité de la International
Standards Office (ISO) ratificó el documento del comité ANSI en 1992, convirtiéndolo ası́
en un estándar internacional. Durante mucho tiempo se conoció a esta versión 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ón que se publicó en 1999, la que se conoce por
C99 y que es la versión estándar de C que estudiaremos.
C ha tenido un gran impacto en el diseño de otros muchos lenguajes. Ha sido, por
ejemplo, la base para definir la sintaxis y ciertos aspectos de la semántica de lenguajes tan
populares como Java y C++.

1.1. C es un lenguaje compilado


Python y C no sólo se diferencian en su sintaxis, también son distintos en el modo en que se
traducen los programas a código de máquina y en el modo en que ejecutamos los programas.

Python es un lenguaje interpretado: para ejecutar un programa Python, suministramos al


intérprete un fichero de texto (tı́picamente con extensión ((.py))) con su código fuente. Si
deseamos ejecutar sumatorio.py, por ejemplo, hemos de escribir python sumatorio.py

Introducción a la Programación con C 3


1.1 C es un lenguaje compilado 2004/02/10-16:33

en la lı́nea de órdenes Unix. Como resultado, el intérprete 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 órdenes, con lo que se repite el proceso completo de traducción y ejecución
paso a paso. Aunque no modifiquemos el código fuente, es necesario interpretarlo (traducir
y ejecutar paso a paso) nuevamente.

sumatorio.py Intérprete Python Resultados

C es un lenguaje compilado: antes de ejecutar un programa escrito por nosotros, sumi-


nistramos su código fuente (en un fichero con extensión ((.c))) a un compilador de C.
El compilador lee y analiza todo el programa. Si el programa está correctamente escrito
según la definición del lenguaje, el compilador genera un nuevo fichero con su traducción
a código de máquina, 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ódigo 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 órdenes 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ón,


ya que cuando el programa se ejecuta está completamente traducido a código de máquina y
se ahorra el proceso de ((traducción simultánea)) que conlleva interpretar un programa. Pero,
además, como se traduce a código de máquina en una fase independiente de la fase de ejecución,
el programa traductor puede dedicar más tiempo a intentar encontrar la mejor traducción
posible, la que proporcione el programa de código de máquina más rápido (o que consuma
menos memoria).
Nosotros usaremos un compilador concreto de C: gcc (en su versión 3.2 o superior)2 . Su
forma de uso más básica es ésta:
gcc fichero.c -o fichero ejecutable
La opción -o es abreviatura de ((output)), es decir, ((salida)), y a ella le sigue el nombre del
fichero que contendrá la traducción a código máquina del programa. Debes tener presente que
dicho fichero sólo se genera si el programa C está correctamente escrito.
Si queremos compilar el programa sumatorio.c hemos de usar una opción especial:
gcc sumatorio.c -lm -o sumatorio

La opción -lm se debe usar siempre que nuestro programa utilice funciones del módulo
matemático (como sqrt, que se usa en sumatorio.c). Ya te indicaremos por qué en la sección
dedicada a presentar el módulo matemático de C.
1 Por 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.
2 La versión 3.2 de gcc es la primera en ofrecer un soporte suficiente de C99. Si usas una versión anterior, es

posible que algunos (pocos) programas del libro no se compilen correctamente.

4 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción 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ún momento da un aviso indicando que no puede
compilar algún 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ún 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ón -std el valor c89, c99, gnu89 o gnu99.

1.2. Traduciendo de Python a C: una guı́a rápida


Empezaremos por presentar de forma concisa cómo 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ón
con sentencias de impresión 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á el nombre de bloque.

2. Toda variable debe declararse antes de ser usada. La declaración 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á 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 }
3 Recuerda que no estudiaremos las variables de tipo cadena hasta el próximo capı́tulo.

Introducción a la Programación con C 5


1.2 Traduciendo de Python a C: una guı́a rápida 2004/02/10-16:33

No es obligatorio que la declaración 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ás 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ón:

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ón C son similares a las sentencias de asignación 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ón que hay a mano derecha. Cada sentencia de asignación
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úmeros 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ón (que en Python era **).
Hay operadores para la conversión 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ómo se disponen los paréntesis: los operadores de conversión 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 }
4 En 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 éste sea anterior al
primer uso de la misma.

6 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

Las reglas de asociatividad y precedencia de los operadores son casi las mismas que apren-
dimos en Python. Hay más operadores en C y los estudiaremos más adelante.
5. Para mostrar resultados por pantalla se usa la función printf . La función recibe uno o
más 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á el
valor o la cantidad de cifras decimales de un número flotante);
y, a continuación, las expresiones cuyos valores se desea mostrar (debe haber una
expresión 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 %f\n", a, b);


12

13 return 0;
14 }

La cadena con formato debe ir encerrada entre comillas dobles, no simples. El carácter
de retorno de carro (\n) es obligatorio si se desea finalizar la impresión 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ón 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ón 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 %f\n", 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ñala 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ón acerca de lo que el
usuario debe introducir, hemos de usar nuevas sentencias printf :

Introducción a la Programación con C 7


1.2 Traduciendo de Python a C: una guı́a rápida 2004/02/10-16:33

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 %f\n", 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ón va encerrada obligatoriamente entre paréntesis;
y el bloque de sentencias cuya ejecución está supeditada a la satisfacción de la con-
dición va encerrado entre llaves (aunque matizaremos esta afirmación más 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ás, es positivo.\n");
14 }
15 }
16

8 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

17 return 0;
18 }

También 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ácil obtener el mismo efecto con una
sucesión 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éntesis alrededor de la condición 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ón a la Programación con C 9


1.2 Traduciendo de Python a C: una guı́a rápida 2004/02/10-16:33

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én 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ódulos 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ón de una biblioteca: debes importar el contenido
completo de la biblioteca.
Las funciones matemáticas pueden importarse del módulo matemático 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ón a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción 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ándolas de bibliotecas. Por ejemplo, abs (valor absolu-
to) puede importarse del módulo stdlib.h (por ((standard library)), es decir, ((biblioteca
estándar))).
Las (aproximaciones a las) constantes π y e se pueden importar de la biblioteca ma-
temática, pero sus identificadores son ahora M_PI y M_E, respectivamente.

No está mal: ya sabes traducir programas Python sencillos a C (aunque no sabemos traducir
programas con definiciones de función, ni con variables de tipo cadena, ni con listas, ni con
registros, ni con acceso a ficheros. . . ). ¿Qué tal practicar con unos pocos ejercicios?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 2 Traduce a C este programa Python.
1 a = int(raw_input(’Dame el primer número: ’))
2 b = int(raw_input(’Dame el segundo número: ’))
3

4 if a >= b:
5 maximo = a
6 else:
7 maximo = b
8

9 print ’El máximo es’, maximo

·3 Traduce a C este programa Python.


1 n = int(raw_input(’Dame un número: ’))
2 m = int(raw_input(’Dame otro número: ’))
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ón a la Programación con C 11


1.3 Estructura tı́pica de un programa C 2004/02/10-16:33

6 print ’Solución: ’, x
7 else:
8 if b != 0:
9 print ’La ecuación no tiene solución.’
10 else:
11 print ’La ecuación 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ón: ’
6 print ’1) Calcular el diámetro.’
7 print ’2) Calcular el perı́metro.’
8 print ’3) Calcular el área.’
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ámetro 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 área es’, area
23 elif opcion < 0 or opcion > 4:
24 print ’Sólo hay cuatro opciones: 1, 2, 3 o 4. Tú 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ás que una colección 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és significa ((principal))) y contiene el código del programa principal. No nos deten-
dremos a explicar la sintaxis de la definición de funciones hasta el capı́tulo 3, pero debes saber
ya que la definición de la función main empieza con ((int main (void))) y sigue con el cuerpo
de la función encerrado entre un par de llaves. La función main debe devolver un valor entero

12 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción 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 ésta:

Importación de funciones, variables, constantes, etc.

Definición de constantes y macros.

Definición de nuevos tipos de datos.

Declaración de variables globales.

Definición de funciones.

int main(void)
{
Declaración de variables propias del programa principal (o sea, locales a main).

Programa principal.

return 0;
}

Un fichero con extensión ((.c)) que no define la función 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ún la versión 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ón ‘_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ón determina los diferentes bloques de un programa, en C la
indentación es absolutamente superflua: indentamos los programas únicamente para hacerlos
más legibles. En C se sabe dónde empieza y dónde acaba un bloque porque éste está encerrado
entre una llave abierta ({) y otra cerrada (}).
He aquı́ un ejemplo de bloques anidados en el que hemos indentado el código 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);
5 El valor 0 se toma, por un convenio, como señal de que el programa finalizó correctamente. El sistema

operativo Unix recibe el valor devuelto con el return y el intérprete de órdenes, por ejemplo, puede tomar una
decisión acerca de qué hacer a continuación en función del valor devuelto.

Introducción a la Programación con C 13


1.4 C es un lenguaje de formato libre 2004/02/10-16:33

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 ("%d\n", 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 ("%d\n", 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ón a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

17 }
18 printf ("%d\n", minimo);
19 return 0;
20 }

De hecho, como if -else es una única sentencia, también 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 ("%d\n", minimo);
17 return 0;
18 }

Debes tener cuidado, no obstante, con las ambigüedades que parece producir un sólo else y
dos if :
primero es minimo 1.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)
11 if (a < c)
12 printf ("El primero es el mı́nimo.\n");
13 else
14 printf ("El primero no es el mı́nimo.\n");
15 printf ("%d\n", minimo);
16 return 0;
17 }

¿Cuál de los dos if se asocia al else? C usa una regla: el else se asocia al if más próximo (en el
ejemplo, el segundo). No obstante, puede resultar más legible que explicites con llaves el alcance
de cada 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) {
11 if (a < c)

Introducción a la Programación con C 15


1.4 C es un lenguaje de formato libre 2004/02/10-16:33

12 printf ("El primero es el mı́nimo.\n");


13 }
14 else
15 printf ("El primero no es el mı́nimo.\n");
16 printf ("%d\n", minimo);
17 return 0;
18 }

Ahora que has adquirido la práctica 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ón no importa. . . pero nadie se pone de acuerdo


En C no es obligatorio indentar los programas, aunque todos los programadores están de
acuerdo en que un programa sin una ((correcta)) indentación es ilegible. ¡Pero no hay consenso
en lo que significa indentar ((correctamente))! Hay varios estilos de indentación en C y cada
grupo de desarrolladores escoge el que más 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) Ídem, 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) Ídem, pero las dos llaves se disponen más a la derecha y el contenido del bloque más a
la derecha:
if (a==1)
{
b = 1;
c = 2;
}
e) Y aún 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ón de puro convenio. Aún ası́, hay más de
una discusión 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ón de la definición de
funciones (como main), que sigue el convenio de indentación que relacionamos en tercer
lugar.

Una norma: las sentencias C acaban con un punto y coma. Y una excepción a la norma: no

16 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción 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é 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ún sentido y hace más difı́cil la
comprensión del programa. Recuerda: vela siempre por la legibilidad de los programas.
También podemos poner más de una sentencia en una misma lı́nea, pues el compilador sabrá
dónde 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 negativo\n");
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 %d\n", 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: %f\n", a, b, s); return 0;}
Obviamente, hubiera sido una mala elección: un programa escrito ası́, aunque correcto, es com-
pletamente ilegible.7
Un programador de C experimentado hubiera escrito sumatorio.c utilizando llaves sólo don-
de resultan necesarias y, probablemente, utilizando unas pocas lı́neas menos. Estudia las dife-
rencias entre la primera versión 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 negativo\nLı́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 %d\n 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: %f\n", a, b, s);
26

27 return 0;
28 }
6 Habrá una excepción a esta norma: las construcciones struct, cuya llave de cierre debe ir seguida de un

punto y coma.
7 Quizá 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ás: cada sentencia #include
debe ocupar una lı́nea y el carácter # debe ser el primero de la lı́nea.

Introducción a la Programación con C 17


1.4 C es un lenguaje de formato libre 2004/02/10-16:33

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ó 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 choo\n" ) ;
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é hace? ¡Sólo imprime en pantalla ((choo choo))!


El siguiente programa es un generador de anagramas escrito por Andreas Gustafsson
(AG ;-)) y se presentó a la edición 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ándar y recibe como argumentos el número de


palabras del anagrama (precedido por un guión) y el texto del que se desea obtener un ana-
grama. Si compilas el programa y lo ejecutas con un diccionario inglés y el texto ((Universitat
Jaume I)) descubrirás algunos anagramas curiosos. Para ver qué hace exactamente, ejecuta

$ anagrama </usr/dict/words -3 universitat jaume i)

en el intérprete de órdenes: por pantalla aparecerán decenas de anagramas, entre ellos


((autism injure vitae)) y ((mutate via injuries)). Usando un diccionario español y di-
ferentes números de palabras obtendrás, entre otros, éstos: ((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ón de honor en el concurso!

Los lenguajes de programación en los que el código 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áles son:
1 #include <stdio.h>

18 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

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 %d\n", 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 %d\n", 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 éste se prolonga hasta el final de lı́nea.
La marca especial no es #, sino //. El segundo tipo de comentario puede ocupar más de una
lı́nea: empieza con los caracteres /* y finaliza con la primera aparición del par de caracteres */.
En este ejemplo destacamos con fondo gris el texto de cada uno de los comentarios:

maximo.c maximo.c
1 /*********************************************************************
2 * Un programa de ejemplo.
3 *--------------------------------------------------------------------
4 * Propósito: 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úmeros.
18 int m; // Variable para el máximo de los tres.
19

20 /* Lectura de un número */
21 printf ("a: "); scanf ("%d", &a);
22 /* ... de otro ... */
23 printf ("b: "); scanf ("%d", &b);
24 /* ... y de otro más. */
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;
29 else //Y en este otro caso b < a ≤ c.

Introducción a la Programación con C 19


1.5 Hay dos tipos de comentario 2004/02/10-16:33

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ón del resultado. */))
37 printf ("El máximo de %d, %d y %d es %d\n", a, b, c, m);
38 return 0;
39 }

Los comentarios encerrados entre /* y */ no se pueden anidar. Este fragmento de programa


es incorrecto:
/* un /* comentario */ mal hecho */
¿Por qué? 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 único 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 %d\n", i, j); */
13 j *= 2;
14 i += 1;
15 }
16 printf ("%d\n", 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ñadimos 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;
10 j = 1;
11 /*
12 while (i <= b) {
13 /* printf ("%d %d\n", i, j); */
14 j *= 2;

20 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

15 i += 1;
16 }
17 */
19 printf ("%d\n", j);
20 return 0;
21 }

Al compilar nuevamente el programa aparecen mensajes de error. ¿Por qué?


· 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 %d\n", i, j);
14 j *= 2;
15 i += 1;
16 }
17 */
19 printf ("%d\n", j);
20 return 0;
21 }
· 13 ¿Cómo 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úmeros, 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úmero entero en C es mediante una secuencias de dı́gitos.
Por ejemplo, 45, 0 o 124653 son enteros. Al igual que en Python, está prohibido insertar espacios
en blanco (o cualquier otro sı́mbolo) entre los dı́gitos de un literal entero.
Hay más formas de expresar enteros. En ciertas aplicaciones resulta útil expresar un número
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úmero 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úmero en base 16 debes usar
el par de caracteres 0x seguido del número en cuestión. El literal 0xff, por ejemplo, codifica el
valor decimal 255.
Pero aún hay una forma más de codificar un entero, una que puede resultar extraña al
principio: mediante un carácter entre comillas simples, que representa a su valor ASCII. El
valor ASCII de la letra ((a minúscula)), 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ódigo ASCII del salto de lı́nea).

Introducción a la Programación con C 21


1.6 Valores literales en C 2004/02/10-16:33

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ón alguna.

1.6.2. Flotantes
Los números en coma flotante siguen la misma sintaxis que los flotantes de Python. Un número
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én lo es, pero porque tiene exponente (es decir,
tiene una letra e seguida de un entero). Ambos representan al número real 20.0. (Recuerda que
2e1 es 2 · 101 .) Es posible combinar en un número parte decimal con exponente: 2.0e1 es un
número en coma flotante válido.

1.6.3. Cadenas
Ası́ como en Python puedes optar por encerrar una cadena entre comillas simples o dobles, en
C sólo 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 ocupa\n dos lı́neas"
7. "una cadena que \\no ocupa dos lı́neas"
.............................................................................................
Te relacionamos las secuencias de escape que puedes necesitar más frecuentemente:

Secuencia Valor
\a (alerta): produce un aviso audible o visible.
\b (backspace, espacio atrás): el cursor retrocede un espacio a la izquierda.
\f (form feed, alimentación de página): pasa a una nueva ((página)).
\n (newline, nueva lı́nea): el cursor pasa a la primera posición de la siguiente
lı́nea.
\r (carriage return, retorno de carro): el cursor pasa a la primera posición
de la lı́nea actual.
\t (tabulador): desplaza el cursor a la siguiente marca de tabulación.
\\ muestra la barra invertida.
\" muestra la comilla doble.
\número octal muestra el carácter cuyo código ASCII (o IsoLatin) es el número octal
indicado. El número octal puede tener uno, dos o tres dı́gitos octales.
Por ejemplo "\60" equivale a "0", pues el valor ASCII del carácter cero
es 48, que en octal es 60.
\xnúmero hexadecimal ı́dem, pero el número está codificado en base 16 y puede tener uno o
dos dı́gitos hexadecimales. Por ejemplo, "\x30" también equivale a "0",
pues 48 en decimal es 30 en hexadecimal.
\? muestra el interrogante.

22 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

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éricos escalares: enteros y flotantes8 . En C hay una gran
variedad de tipos escalares en función del número de cifras o de la precisión con la que deseamos
trabajar, ası́ que nos permite tomar decisiones acerca del compromiso entre rango/precisión y
ocupación de memoria: a menor rango/precisión, menor ocupación 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éndice A. Encontrarás una variedad enorme: enteros con diferente número de bits, con y sin
signo, flotantes de precisión 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úmeros enteros. La especificación
de C no define el rango de valores que podemos representar con una variable de tipo int, es
decir, no define el número de bits que ocupa una variable de tipo int. No obstante, lo más
frecuente es que ocupe 32 bits. Nosotros asumiremos en este texto que el tamaño 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álogo de tipos que presentamos en el apéndice A.
En C, tradicionalmente, los valores enteros se han utilizado para codificar valores booleanos.
El valor 0 representa el valor lógico ((falso)) y cualquier otro valor representa ((cierto)). En la
última revisión 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é desperdiciar el bit más significativo en una variable entera de 32 bits que nunca
almacenará valores negativos? C te permite definir variables de tipo ((entero sin signo)). El tipo
tiene un nombre compuesto por dos palabras: ((unsigned int)) (aunque la palabra unsigned,
sin más, es sinónimo 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úmeros en coma flotante de 32 bits. La codificación de coma
flotante permite definir valores con decimales. El máximo 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úmero no nulo más pequeño (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ón, pues, de que podemos representar números con 8 decimales. No es ası́: la
precisión no es la misma para todos los valores: es tanto mayor cuanto más próximo a cero es
el valor.
8 Bueno, esos son los que hemos estudiado. Python tiene, además, enteros largos. Otro tipo numérico no

secuencial de Python es el complejo.

Introducción a la Programación con C 23


1.7 C tiene un rico juego de tipos escalares 2004/02/10-16:33

1.7.4. El tipo char


El tipo char, aunque tenga un nombre que parezca sugerir el término ((carácter)) (que en inglés
es ((character))) designa en realidad a una variante de enteros: el conjunto de números 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ón a=’0’ es absolutamente equivalente a la asignación 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ón para enteros de 32 bits sin signo, hay una versión de
char sin signo: unsigned char. Con un unsigned char se puede representar cualquier entero
en el rango [0, 255].

C, ocupación de los datos, complemento a 2 y portabilidad


Los números 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ás. Los valores negativos se codifican representando en binario su valor
absoluto, invirtiendo todos sus bits y añadiendo 1 al resultado. Supón 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ón binaria de 28, invirtiendo sus bits (11100011), y
añadiendo uno. El resultado es 11100100.
Una ventaja de la notación en complemento a 2 es que simplifica el diseño de circuitos
para la realización de cálculos aritméticos. 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ónica
utilizada para efectuar sumas convencionales:

00011110
+ 11100100
00000010

El complemento a 2 puede gastarte malas pasadas si no eres consciente de cómo fun-


ciona. Por ejemplo, sumar dos números 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ómeno se conoce como ((desbordamiento)). C no aborta la ejecución 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á produciendo resultados erróneos.
El estándar de C no define de modo claro la ocupación de cada uno de sus tipos de datos
lo cual, unido a fenómenos 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ás antiguos era frecuente que ocupara
sólo 16. Un programa que suponga una representación mayor que la real puede resultar en
la comisión de errores en tiempo de ejecución. Por ejemplo, si una variable a de tipo int
ocupa 32 bits y vale 32767, ejecutar la asignación a = a + 1 almacenará 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ún, puede que el error pase inadvertido durante mucho tiem-
po: el programa no abortará la ejecución y producirá 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ón en el que se ha escrito buena
parte de los programas que hoy ejecutamos en una gran variedad de plataformas.

24 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

char, unsigned char, ASCII e IsoLatin1


La tabla ASCII tiene caracteres asociados a valores comprendidos entre 0 y 127, ası́ que
todo carácter ASCII puede almacenarse en una variable de tipo char. Pero, en realidad,
nosotros no usamos la tabla ASCII ((pura)), sino una extensión suya: IsoLatin1 (también
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ánicas occidentales. ¿Qué ocurre si asignamos a una variable de tipo char el carácter
’á’? El código IsoLatin1 de ’á’ es 225, que es un valor numérico mayor que 127, el máximo
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.

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álidos


Las reglas para construir identificadores válidos son las mismas que sigue Python: un identifi-
cador es una sucesión de letras (del alfabeto inglés), dı́gitos y/o el carácter de subrayado ( )
cuyo primer carácter no es un dı́gito. Y al igual que en Python, no puedes usar una palabra
reservada como identificador. He aquı́ la relación 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ón


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ácter) y otra de tipo flotante:
int a;
char b;
float c;

Se puede declarar una serie de variables del mismo tipo en una sola sentencia de declaración
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ólo 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 ;

Introducción a la Programación con C 25


1.9 Salida por pantalla 2004/02/10-16:33

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ón con inicialización


Debes tener presente que el valor inicial de una variable declarada está indefinido. Jamás debes
acceder al contenido de una variable que no haya sido previamente inicializada. Si lo haces,
el compilador no detectará error alguno, pero tu programa presentará un comportamiento
indeterminado: a veces funcionará 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ón de un programa cuando se intentaba
usar una variable no inicializada; C no aborta la ejecución, pero presenta un comportamiento
indeterminado.
Puedes inicializar las variables en el momento de su declaración. Para ello, basta con añadir
el operador de asignación y un valor a continuación de la variable en cuestión.
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 él, las variables a, b y d se inicializan en la declaración 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úmbrate
a inicializar las variables tan pronto puedas.

1.9. Salida por pantalla


La función de impresión de información en pantalla utilizada habitualmente es printf . Es una
función disponible al incluir stdio.h en el programa. El uso de printf es ligeramente más com-
plicado que el de la sentencia print de Python, aunque no te resultará difı́cil si ya has aprendido
a utilizar el operador de formato en Python (%).
En su forma de uso más 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 }

26 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

La función printf no añade un salto de lı́nea automáticamente, como sı́ hacı́a print en Python.
En el programa anterior, ambas cadenas se muestran una a continuación de otra. Si deseas que
haya un salto de lı́nea, deberás escribir \n al final de la cadena.
1 #include <stdio.h>
2

3 int main(void)
4 {
5 printf ("Una cadena\n");
6 printf ("y otra.\n");
7 return 0;
8 }

1.9.1. Marcas de formato para la impresión de valores con printf


Marcas de formato para números
Para mostrar números 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ún y que también 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ón printf :
printf ("Un entero: %d, un flotante: %f, un byte: %hhd\n", 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á por pantalla esta llamada a printf suponiendo que a es de tipo entero y
vale 10?
printf ("%d-%d\n", a+1, 2+2);
.............................................................................................
Las marcas de formato para enteros aceptan modificadores, es decir, puedes alterar la repre-
sentación introduciendo ciertos caracteres entre el sı́mbolo de porcentaje y el resto de la marca.
Aquı́ tienes los principales:
Un número positivo: reserva un número 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úmero negativo: reserva tantos espacios como indique el valor absoluto del número
para representar el entero y muestra el valor alineado a la izquierda.
Ejemplo: la sentencia

Introducción a la Programación con C 27


1.9 Salida por pantalla 2004/02/10-16:33

printf ("[%-6d]", 10);

muestra en pantalla:
[10 ]

Un número que empieza por cero: reserva tantos espacios como indique el valor absoluto
del número para representar el entero y muestra el valor alineado a la izquierda. 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ón de flotantes que podemos seleccionar
mediante la marca de formato adecuada:
Tipo Notación Marca
float Convencional %f
float Cientı́fica %e

La forma convencional muestra los números con una parte entera y una decimal separadas
por un punto. La notación cientı́fica representa al número 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ón cientı́fica, el número 10.1 se representa con 1.010000e+01 y se interpreta ası́: 1.01×101 .
También puedes usar modificadores para controlar la representación en pantalla de los flotan-
tes. Los modificadores que hemos presentado para los enteros son válidos aquı́. Tienes, además,
la posibilidad de fijar la precisión:
Un punto seguido de un número: indica cuántos decimales se mostrarán.
Ejemplo: la sentencia

printf ("[%6.2f]", 10.1);

muestra en pantalla:
[ 10.10]

Marcas de formato para texto


Y aún nos queda presentar las marcas de formato para texto. C distingue entre caracteres y
cadenas:
Tipo Marca
carácter %c
cadena %s

¡Atención! La marca %c muestra como carácter un número entero. Naturalmente, el carácter


que se muestra es el que corresponde al valor entero según la tabla ASCII (o, en tu ordenador,
IsoLatin1 si el número es mayor que 127). Por ejemplo, la sentencia

28 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

printf ("[%c]", 97);

muestra en pantalla:
[a]

Recuerda que el valor 97 también puede representarse con el literal ’a’, ası́ que esta otra
sentencia
printf ("[%c]", ’a’);

también muestra en pantalla esto:


[a]

Aún no sabemos almacenar cadenas en variables, ası́ que poca aplicación 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én puedes usar números 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ón 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é muestra por pantalla cada uno de estos programas?
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 }

Introducción a la Programación con C 29


1.9 Salida por pantalla 2004/02/10-16:33

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úscula.
7 printf ("%d ", (int) i);
8 printf ("\n");
9 return 0;
10 }

· 17 Diseña un programa que muestre la tabla ASCII desde su elemento de código numérico
32 hasta el de código numérico 126. En la tabla se mostrarán los códigos ASCII, además de
las respectivas representaciones como caracteres de sus elementos. Aquı́ tienes las primeras y
últimas lı́neas de la tabla que debes mostrar (debes hacer que tu programa muestre la informa-
ción exactamente como se muestra aquı́):
+---------+----------+
| Decimal | Carácter |
+---------+----------+
| 32 | |
| 33 | ! |
| 34 | " |
| 35 | # |
| 36 | $ |
| 37 | % |

30 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

... ...

| 124 | | |
| 125 | } |
| 126 | ~ |
+---------+----------+
.............................................................................................
Hay un rico juego de marcas de formato y las recogemos en el apéndice A. Consúltalo 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ón de celdas numeradas y que una dirección de
memoria no es más que un número entero. La declaración 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á
(codificado en complemento a 2) el valor de dicha variable. Modificar el valor de la variable
mediante una asignación supone modificar el patrón 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ı́:

996: 01010010 10101000 01110011 11110010


1000: 01011010 00111101 00111010 11010111 a
1004: 10111011 10010110 01010010 01010011 b
1008: 11010111 01000110 11110010 01011101

Observa que, inicialmente, cuando se reserva la memoria, ésta contiene un patrón de bits
arbitrario. La sentencia a = 0 se interpreta como ((almacena el valor 0 en la dirección de memoria
de a)), es decir, ((almacena el valor 0 en la dirección de memoria 1000))10 . Este es el resultado
de ejecutar esa sentencia:
9 En 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.
10 En 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ólo con la dirección del primero de ellos.

Introducción a la Programación con C 31


1.10 Variables y direcciones de memoria 2004/02/10-16:33

996: 01010010 10101000 01110011 11110010


1000: 00000000 00000000 00000000 00000000 a
1004: 10111011 10010110 01010010 01010011 b
1008: 11010111 01000110 11110010 01011101

La asignación b = a + 8 se interpreta como ((calcula el valor que resulta de sumar 8 al contenido


de la dirección de memoria 1000 y deja el resultado en la dirección de memoria 1004)).

996: 01010010 10101000 01110011 11110010


1000: 00000000 00000000 00000000 00000000 a
1004: 00000000 00000000 00000000 00001000 b
1008: 11010111 01000110 11110010 01011101

Hemos supuesto que a está en la dirección 1000 y b en la 1004, pero ¿podemos saber en qué
direcciones de memoria se almacenan realmente a y b? Sı́: el operador & permite conocer la
dirección 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ón de a: %u\n", (unsigned int)&a);


11 printf ("Dirección de b: %u\n", (unsigned int)&b);
12

13 return 0;
14 }

Observa que usamos la marca de formato %u para mostrar el valor de la dirección de memoria,
pues debe mostrarse como entero sin signo. La conversión 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ú
mismo el programa obtengas un resultado diferente):

Dirección de a: 3221222580
Dirección de b: 3221222576

O sea, que en realidad este otro gráfico representa mejor la disposición de las variables en
memoria:

3221222572: 01010010 10101000 01110011 11110010


3221222576: 00000000 00000000 00000000 00001000 b
3221222580: 00000000 00000000 00000000 00000000 a
3221222584: 11010111 01000110 11110010 01011101

11 Hay un marca especial, %p, que muestra directamente la dirección de memoria sin necesidad de efectuar la

conversión a unsigned int, pero lo hace usando notación hexadecimal.

32 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

Normalmente no necesitamos saber en qué dirección de memoria se almacena una variable,


ası́ que no recurriremos a representaciones gráficas 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ás cómodamente legible que como una secuencia de
bits. La representación anterior se simplificará, pues, ası́:

a 0

b 8

Las direcciones de memoria de las variables se representarán con flechas que apuntan a sus
correspondientes cajas:
&a
a 0
&b

b 8

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ómo se interpreta la sentencia de asignación a = 0? Se interpreta como ((almacena el valor


0 en la dirección de memoria de a)). ¿Y b = a?, ¿cómo se interpreta? Como ((almacena una
copia del contenido de a en la dirección de memoria de b)). Fı́jate bien, el identificador a recibe
interpretaciones diferentes según aparezca a la izquierda o a la derecha de una asignación:

a la izquierda del igual, significa ((la dirección de a)),

y a la derecha, es decir, en una expresión, significa ((el contenido de a)).

La función scanf necesita una dirección de memoria para saber dónde debe depositar un
resultado. Como no estamos en una sentencia de asignación, sino en una expresión, es necesario
que obtengamos explı́citamente la dirección 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ón scanf , disponible al incluir stdio.h, permite leer datos por teclado. La función scanf
se usa de un modo similar a printf : su primer argumento es una cadena con marcas de formato.
A éste le siguen una o más direcciones de memoria. Si deseas leer por teclado el valor de una
variable entera a, puedes hacerlo ası́:
scanf ("%d", &a);

Introducción a la Programación con C 33


1.12 Expresiones 2004/02/10-16:33

Observa que la variable cuyo valor se lee por teclado va obligatoriamente precedida por el
operador &: es ası́ como obtenemos la dirección de memoria en la que se almacena el valor de
la variable. Uno de los errores que cometerás con mayor frecuencia es omitir el carácter & que
debe preceder a todas las variables escalares en scanf .
Recuerda: la función scanf recibe estos datos:
Una cadena cuya marca de formato indica de qué 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ácter %c
unsigned char como entero %hhu
unsigned char como carácter %c

La dirección de memoria que corresponde al lugar en el que se depositará el valor leı́do.


Debemos proporcionar una dirección 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ácter. En el segundo caso, se espera que el
usuario teclee un solo carácter y se almacenará en la variable su valor numérico según la tabla
ASCII o su extensión IsoLatin.

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ón de memoria
en la que dejará el valor leı́do. Este programa, por ejemplo, es erróneo:

scanf ("%d", a);

La función scanf no está recibiendo la dirección de memoria en la que ((reside)) a, sino el


valor almacenado en a. Si scanf interpreta dicho valor como una dirección de memoria (cosa
que hace), guardará en ella el número que lea de teclado. ¡Y el compilador no necesariamente
detectará el error! El resultado es catastrófico.
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óneo:

scanf ("%d", &c);

A scanf le estamos pasando la dirección de memoria de la variable c. Hasta ahı́, bien. Pero
c sólo ocupa un byte y a scanf le estamos diciendo que ((rellene)) 4 bytes con un número
entero a partir de esa dirección de memoria. Otro error de consecuencias gravı́simas. La
marca de formato adecuada para leer un número de tipo char hubiera sido %hhd.

scanf ("%hhd", &c);

Una advertencia: la lectura de teclado en C presenta numerosas dificultades prácticas. Es


muy recomendable que leas el apéndice 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án un comportamiento muy extraño y no entenderás por qué. Tú 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

34 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

-Wall
Cuando escribimos un texto en castellano podemos cometer tres tipos de errores:
Errores léxicos: escribimos palabras incorrectamente, con errores ortográficos, o usa-
mos palabras inexistentes. Por ejemplo: ((herror)), ((lécsico)), ((jerigóndor)).
Errores sintácticos: aunque las palabras son válidas y están 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ón ade-
cuada, etc. Por ejemplo: ((el error sintáctica son)), ((la compilador detectó
errores)).
Errores semánticos: la frase está correctamente construida pero carece de significado
válido en el lenguaje. Por ejemplo: ((el compilador silbó 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éxicos: usamos carácteres no válidos o construimos incorrectamente compo-
nentes elementales del programa (como identificadores, cadenas, palabras clave, etc.).
Por ejemplo: ((@3)), (("una cadena sin cerrar)).
Errores sintácticos: construı́mos mal una sentencia aunque usamos palabras válidas.
Por ejemplo: ((while a < 10 { a += 1; })), ((b = 2 * / 3;)).
Errores semánticos: la sentencia no tiene un significado ((válido)). Por ejemplo, si a
es de tipo float, estas sentencias contienen errores semánticos: ((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á 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éxico o sintáctico: cuando lo detecta, nos
informa del error y no genera traducción a código de máquina del programa. Con los errores
semánticos, sin embargo, el compilador es más indulgente: la filosofı́a de C es suponer que
el programador puede tener una buena razón 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ún qué posibles errores, el compilador puede emitir avisos (warnings).
Es posible regular hasta qué punto deseamos que el compilador nos proporcione avisos.
La opción -Wall (((Warning all)), que significa ((todos los avisos))) activa la detección de
posibles errores semánticos, notificándolos como avisos. Este programa erróneo, por ejemplo,
no genera ningún 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ánticos en las lı́neas 5 y 6. Te hará falta bastante


práctica para aprender a descifrar mensajes tan parcos o extraños 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ómalo y no hayas detectado errores léxicos o
sintácticos.)

y asociatividad en la tabla de la página 39.) Presta especial atención a los operadores que no

Introducción a la Programación con C 35


1.12 Expresiones 2004/02/10-16:33

Lecturas múltiples con scanf


No te hemos contado todo sobre scanf . Puedes usar scanf para leer más 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 %d\n", a, b);
9 return 0;
10 }

También podemos especificar con cierto detalle cómo esperamos que el usuario introduzca
la información. Por ejemplo, con scanf ("%d-%d", &a, &b) indicamos que el usuario de-
be separar los enteros con un guión; y con scanf ("(%d,%d)", &a, &b) especificamos que
esperamos encontrar los enteros encerrados entre paréntesis y separados por comas.
Lee la página de manual de scanf (escribiendo man 3 scanf en el intérprete de órdenes
Unix) para obtener más información.

conoces por el lenguaje de programación Python, como son los operadores de bits, el operador
condicional o los de incremento/decremento.

Operadores aritméticos Suma (+), resta (-), producto (*), división (/), módulo o resto de
la división (%), identidad (+ unario), cambio de signo (- unario).
No hay operador de exponenciación.12
La división de dos números enteros proporciona un resultado de tipo entero (como ocurrı́a
en Python).
Los operadores aritméticos sólo funcionan con datos numéricos13 . No es posible, por ejem-
plo, concatenar cadenas con el operador + (cosa que sı́ podı́amos hacer en Python).
La dualidad carácter-entero del tipo char hace que puedas utilizar la suma o la resta
(o cualquier otro operador aritmético) con variables o valores de tipo char. Por ejemplo
’a’ + 1 es una expresión válida y su valor es ’b’ (o, equivalentemente, el valor 98, ya que
’a’ equivale a 97). (Recuerda, no obstante, que un carácter no es una cadena en C, ası́
que "a" + 1 no es "b".)

Operadores lógicos Negación o no-lógica (!), y-lógica o conjunción (&&) y o-lógica o disyun-
ción (||).
Los sı́mbolos son diferentes de los que aprendimos en Python. La negación era allı́ not,
la conjunción era and y la disyunción 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ógico, igual que en Python.

Operadores de comparación 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ólo puedes usarlos para
comparar valores escalares. No puedes, por ejemplo, comparar cadenas mediante estos
operadores.
La evaluación de una comparación 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).
12 Pero hay una función de la biblioteca matemática que permite calcular la potencia de un número: pow .
13 Yla suma y la resta trabajan también con punteros. Ya estudiaremos la denominada ((aritmética de punteros))
más adelante.

36 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

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én están disponibles en Python, no los estudiamos entonces porque son de uso
infrecuente en ese lenguaje de programación.
El operador de complemento es unario e invierte todos los bits del valor. Tanto & como |
y ^ son operadores binarios. El operador & devuelve un valor cuyo n-ésimo bit es 1 si y
sólo si los dos bits de la n-ésima posición de los operandos son también 1. El operador |
devuelve 0 en un bit si y solo si los correspondientes bits en los operandos son también
0. El operador ^ devuelve 1 si y sólo si los correspondientes bits en los operandos son
diferentes. Lo entenderás 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úmero 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úmero
con signo está codificado en complemento a 2, ası́ que si su primer bit es 1, el número es
negativo).
Los operadores de desplazamiento desplazan los bits un número 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ón de sistemas


C presenta una enorme colección de operadores, pero quizá los que te resulten más llamativos
sean los operadores de bits. Difı́cilmente los utilizarás en programas convencionales, pero
son insustituibles en la programación de sistemas. Cuando manejes información 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ón permita detener la actividad de una impresora
conectada a un puerto paralelo, o que el bit más significativo nos alerte de si falta papel en
la impresora.
Si deseas saber si un bit está o no activo, puedes utilizar los operadores & y <<. Para saber,
por ejemplo, si el octavo bit de una variable x está activo, puedes calcular x & (1 << 7). Si
el resultado es cero, el bit no está activo; en caso contrario, está 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ódigo de un sistema operativo.

Operadores de asignación Asignación (=), asignación con suma (+=), asignación con resta
(-=), asignación con producto (*=), asignación con división (/=), asignación con módulo
(%=), asignación con desplazamiento a izquierda (<<=), asignación con desplazamiento
a derecha (>>=), asignación con ((y)) (&=), asignación con ((o)) (|=), asignación con ((o))
exclusiva (^=).
Puede resultarte extraño que la asignación se considere también un operador. Que sea un
operador permite escribir asignaciones múltiples como ésta:

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ón 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én a a.

Introducción a la Programación con C 37


1.12 Expresiones 2004/02/10-16:33

La asignación con una operación ((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ón con operación recibe el nombre de asignación aumentada.
Operador de tamaño sizeof .
El operador sizeof puede aplicarse a un nombre de tipo (encerrado entre paréntesis) o
a un identificador de variable. En el primer caso devuelve el número de bytes que ocupa
en memoria una variable de ese tipo, y en el segundo, el número 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ón o conversión de tipos (en inglés ((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ón 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ón.


Es similar a la función float de Python, sólo que en Python se hacı́a la conversión con
una llamada a función como float(b), y aquı́ utilizamos un operador prefijo: (float) b. Es
una notación bastante extraña, ası́ que es probable que te confunda durante un tiempo.
En la siguiente sección abundaremos en la cuestión de la conversión de tipos en C.
Operador condicional (?:).
Este operador no tiene correlato en Python. Hay tres operandos: una condición y dos ex-
presiones14 . El resultado de la operación es el valor de la primera expresión si la condición
es cierta y el valor de la segunda si es falsa. Por ejemplo, la asignación

a = (x > 10) ? 100 : 200

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ás (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á 2 después de ejecutar ++i o i++, y valdrá 0 después de ejecutar --i o i--.
Hay una diferencia importante entre aplicar estos operadores en forma prefija o sufija.
14 Lo cierto es que hay tres expresiones, pues la comparación no es más que una expresión. Si dicha expresión

devuelve el valor 0, se interpreta el resultado como ((falso)); en caso contrario, el resultado es ((cierto)).

38 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

La expresión i++ primero se evalúa como el valor actual de i y después hace que i
incremente su valor en una unidad.
La expresión ++i primero incrementa el valor de i en una unidad y después se evalúa
como el valor actual (que es el que resulta de efectuar el incremento).
Si el operador se está aplicando en una expresión, esta diferencia tiene importancia. Su-
pongamos que i vale 1 y que evaluamos esta asignación:

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és se incrementa i.
Al ejecutar esta otra asignación 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és 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ás que nunca necesitarás hilar tan fino. Si es ası́, te equivocas:
en los próximos 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ás de C.

C++
Ya debes entender de dónde viene el nombre C++: es un C ((incrementado)), o sea, mejorado.
En realidad C++ es mucho más que un C con algunas mejoras: es un lenguaje orientado a
objetos, ası́ que facilita el diseño 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ún no te hemos presen-
tado con detalle) ordenados por precedencia (de mayor a menor) y con su aridad (número de
operandos) y asociatividad:

Operador Aridad Asociatividad


() [] -> . 2 izquierda
! ~ ++ -- + - sizeof * & (tipo) 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é valor tiene cada variable tras ejecutar esta secuencia de asignaciones?

Introducción a la Programación con C 39


1.12 Expresiones 2004/02/10-16:33

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é 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ás: "); scanf ("%d", &c);
10

11 r = (a < b) ? ( (a < c) ? a : c ) : ( (b < c) ? b : c );


12

13 printf ("Resultado: %d\n", 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ón.)
· 22 Diseña un programa C que solicite la longitud del lado de un cuadrado y muestre por
pantalla su perı́metro y su área.
· 23 Diseña un programa C que solicite la longitud de los dos lados de un rectángulo y
muestre por pantalla su perı́metro y su área.
· 24 Este programa C es problemático:

un misterio.c un misterio.c
1 #include <stdio.h>
2

3 int main(void)
4 {
5 int a, b;
6

7 a = 2147483647;
8 b = a + a;
9 printf ("%d\n", a);
10 printf ("%d\n", b);
11 return 0;
12 }

Al compilarlo y ejecutarlo hemos obtenido la siguiente salida por pantalla:


2147483647
-2

¿Qué ha ocurrido?
· 25 Diseña un programa C que solicite el radio r de una circunferencia y muestre por
pantalla su perı́metro (2πr) y su área (πr2 ).
· 26 Si a es una variable de tipo char con el valor 127, ¿qué vale ~a? ¿Y qué vale !a? Y si a
es una variable de tipo unsigned int con el valor 2147483647, ¿qué vale ~a? ¿Y qué vale !a?

40 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

· 27 ¿Qué resulta de evaluar cada una de estas dos expresiones?

a) 1 && !!!(0 || 1) || !(0 || 1)

b) 1 & ~~~(0 | 1) | ~(0 | 1)

· 28 ¿Por qué si a es una variable entera a / 2 proporciona el mismo resultado que a >> 1?
¿Con qué operación de bits puedes calcular a * 2? ¿Y a / 32? ¿Y a * 128?
· 29 ¿Qué 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: %hhu\n", a);


14 printf ("Valor de b: %hhu\n", b);
15

16 return 0;
17 }

(Nota: la forma en que hace lo que hace viene de un viejo truco de la programación en
ensamblador,
. . . . . . . . . . . . . .donde
. . . . . . hay
. . . . .ricos
. . . . .juegos
. . . . . . .de
. . .instrucciones
. . . . . . . . . . . . .para
. . . . .la
. . .manipulación
. . . . . . . . . . . . . .de
. . datos
. . . . . . bit
. . . .a. .bit.)
....

¿5 > 3 > 2?
Recuerda que en Python podı́amos combinar operadores de comparación para formar ex-
presiones como 5 > 3 > 2. Esa, en particular, se evalúa a True, pues 5 es mayor que 3 y 3
es menor que 2. C también acepta esa expresión, pero con un significado completamente
diferente basado en la asociatividad por la izquierda del operador >: en primer lugar evalúa
la subexpresión 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ón es 0, o sea, ((falso)).
¡Ojo con la interferencia entre ambos lenguajes! Problemas como éste surgirán con fre-
cuencia cuando aprendas nuevos lenguajes: construcciones que significan algo en el lenguaje
que conoces bien tienen un significado diferente en el nuevo.

1.13. Conversión implı́cita y explı́cita de tipos


El sistema de tipos escalares es más rı́gido que el de Python, aunque más rico. Cuando se evalúa
una expresión y el resultado se asigna a una variable, has de tener en cuenta el tipo de todos
los operandos y también el de la variable en la que se almacena.
Ilustraremos el comportamiento de C con fragmentos de programa que utilizan estas varia-
bles:
char c;
int i;
float x;

Si asignas a un entero int el valor de un entero más corto, como un char, el entero corto
promociona a un entero int automáticamente. Es decir, es posible efectuar esta asignación sin
riesgo alguno:
i = c;

Introducción a la Programación con C 41


1.13 Conversión implı́cita y explı́cita de tipos 2004/02/10-16:33

Podemos igualmente asignar un entero int a un char. C se encarga de hacer la conversión de


tipos pertinente:
c = i;

Pero, ¿cómo? ¡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ás. La conversión funciona
correctamente, es decir, preserva el valor, sólo si el número almacenado en i está 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 %hhd\n", c, d);
13

14 return 0;
15 }

Produce esta salida por pantalla:


0 127

¿Por qué el primer resultado es 0? El valor 512, almacenado en una variable de tipo int,
se representa con este patrón de bits: 00000000000000000000001000000000. Sus 8 bits menos
significativos se almacenan en la variable c al ejecutar la asignación c = a, es decir, c almacena
el patrón de bits 00000000, que es el valor decimal 0.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 30 ¿Qué mostrará por pantalla este programa?
otra conversion delicada.c otra conversion delicada.c
1 #include <stdio.h>
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 %hhd\n", c, d);
16 printf ("%hhu %hhu\n", 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ón almacena en x el valor 2.0 (no el entero 2).
x = 2;

42 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

Si asignamos un valor flotante a un entero, el flotante se convierte en su equivalente entero


(¡si lo hay!). Por ejemplo, la siguiente asignación almacena el valor 2 en i (no el flotante 2.0).
i = 2.0;
Y esta otra asignación almacena en i el valor 0:
i = 0.1;

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 31 ¿Qué 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; c) i = 2 / 4; e) x = 2.0 / 4.0; g) x = 2 / 4;
b) i = 1 / 2; d) i = 2.0 / 4; f) x = 2.0 / 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ón de tipo. Para ello, debes
preceder el valor a convertir con el tipo de destino encerrado entre paréntesis. Ası́:
i = (int) 2.3;
En este ejemplo da igual poner (int) que no ponerlo: C hubiera hecho la conversión implı́ci-
tamente. El término (int) es el operador de conversión a enteros de tipo int. Hay un operador
de conversión para cada tipo: (char), (unsigned int) (float), etc. . . Recuerda que el sı́mbolo
(tipo) es un operador unario conocido como operador de coerción o conversión de tipos.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 32 ¿Qué valor se almacena en las variables i (de tipo int) y x (de tipo float) tras ejecutar
estas sentencias?
a) i = (float) 2; e) x = 2.0 / (int) 4.0; i) x = (float) (1 / 2);
b) i = 1 / (float) 2; f) x = (int) 2.0 / 4; j) x = 1 / (float) 2;
c) i = (int) (2 / 4); g) x = (int) (2.0 / 4);
d) i = (int) 2. / (float) 4; h) x = 2 / (float) 4;
.............................................................................................

1.14. Las directivas y el preprocesador


Las lı́neas que empiezan con una palabra predecida por el carácter # 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é 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ón de C a código de máquina. De momento, nos interesa considerar dos de ellas:
el preprocesador,
y el traductor de C a código de máquina (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és ((include)) significa
((incluye))). El compilador, pues, no llega a ver la directiva, sino el resultado de su sustitución.
Nosotros sólo 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.

Introducción a la Programación con C 43


1.15 Constantes 2004/02/10-16:33

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ólo puede contener el valor de una constante.


Por ejemplo, podemos definir los valores aproximados de π y del número 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ón 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ás de una vez el error de
escribir el operador de asignación o el punto y coma.
No es obligatorio que el nombre de la constante se escriba en mayúsculas, 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ón para
indicar que su valor no se modificará nunca.

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

8 printf ("Radio: ");


9 scanf ("%f", &r);
10

11 a = pi * r * r;
12

13 printf ("Área: %f\n", 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ón en todo lugar donde conviene usar una constante. Más adelante, al
estudiar la declaración de vectores, nos referiremos nuevamente a esta cuestión.

1.15.3. Con tipos enumerados


Es frecuente definir una serie de constantes con valores consecutivos. Imagina una aplicación
en la que escogemos una opción de un menú como éste:
15 Lo de ((en principio)) está justificado. No es cierto que las constantes de C sean variables. Lee el cuadro

titulado ((El preprocesador y las constantes)) para saber qué son exactamente.
16 ¿Has leı́do ya el cuadro ((El preprocesador y las constantes))?
17 ¿A qué esperas para leer el cuadro ((El preprocesador y las constantes))?

44 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

El preprocesador y las constantes


Como te dijimos antes, el compilador de C no compila directamente nuestros ficheros con
extensión ((.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ón
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 entender el porqué 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ón del identificador PI ha sido sustituida por el texto 3.14. Un error tı́pico es
confundir un #define con una declaración normal de variables y, en consecuencia, poner
una asignación 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é? 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!

1) Cargar registros
2) Guardar registros
3) A~
nadir registro
4) Borrar registro
5) Modificar registro
6) Buscar registro
7) Finalizar

Introducción a la Programación con C 45


1.15 Constantes 2004/02/10-16:33

Cuando el usuario escoge una opción, la almacenamos en una variable (llamémosla opcion) y
seleccionamos las sentencias a ejecutar con una serie de comparaciones como las que se muestran
aquı́ esquemáticamente18 :
if (opcion == 1) {
Código para cargar registros
}
else if (opcion == 2) {
Código para guardar registros
}
else if (opcion == 3) {
...
El código resulta un tanto ilegible porque no vemos la relación entre los valores numéricos y las
opciones de menú. Es frecuente no usar los literales numéricos 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ódigo para cargar registros
}
else if (opcion == GUARDAR) {
Código para guardar registros
}
else if (opcion == ANYADIR) {
...

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ódigo para cargar registros
}
else if (opcion == Guardar ) {
Código para guardar registros
}
else if (opcion == Anyadir ) {
...

La primera lı́nea define los valores Cargar , Guardar , . . . como una sucesión de valores
correlativos. La asignación del valor 1 al primer elemento de la enumeración hace que la sucesión
empiece en 1. Si no la hubiésemos escrito, la sucesión empezarı́a en 0.
Es habitual que los enum aparezcan al principio del programa, tras la aparición de los
#include y #define.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 33 ¿Qué valor tiene cada identificador de este tipo enumerado?
enum { Primera=’a’, Segunda, Tercera, Penultima=’y’, Ultima };

(No te hemos explicado qué hace la segunda asignación. Comprueba que la explicación que das
es correcta con un programa que muestre por pantalla el valor de cada identificador.)
18 Más adelante estudiaremos una estructura de selección que no es if y que se usa normalmente para especificar

este tipo de acciones.

46 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

.............................................................................................
Los tipos enumerados sirven para algo más que asignar valores a opciones de menú. Es
posible definir identificadores con diferentes valores para series de elementos como los dı́as de
la semana, los meses del año, etc.
enum { Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo };
enum { Invierno, Primavera, Verano, Otonyo };
enum { Rojo, Verde, Azul };

1.16. Las bibliotecas (módulos) se importan con #include


En C, los módulos reciben el nombre de bibliotecas (o librerı́as, como traducción fonéticamente
similar del inglés library). La primera lı́nea de sumatorio.c es ésta:
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án declaradas en el fichero stdio.h, que es abreviatura de ((standard
input/output)) (entrada/salida estándar). En particular, el programa sumatorio.c usa las fun-
ciones printf y scanf de stdio.h. Los ficheros con extensión ((.h)) se denominan ficheros cabecera
(la letra h es abreviatura de ((header)), que en inglés 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ódulo import *.
Normalmente no basta con incluir un fichero de cabecera con #include para poder compilar
un programa que utiliza bibliotecas. Es necesario, además, compilar con opciones especiales.
Abundaremos sobre esta cuestión inmediatamente, al presentar la librerı́a matemática.

1.16.1. La biblioteca matemática


Podemos trabajar con funciones matemáticas incluyendo math.h en nuestros programas. La
tabla 1.1 relaciona algunas de las funciones que ofrece la biblioteca matemática.

Función C Función matemática


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úmero 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ás próximo a x
ceil (x) redondeo superior de x
floor (x) redondeo inferior de x

Tabla 1.1: Algunas funciones matemáticas disponibles en la biblioteca math.h.

Todos los argumentos de las funciones de math.h son de tipo flotante.19


La biblioteca matemática también ofrece algunas constantes matemáticas predefinidas. Te
relacionamos algunas en la tabla 1.2.
19 Lo cierto es que son de tipo double (véase el apéndice A), pero no hay problema si las usas con valores y

variables de tipo float, ya que hay conversión automática de tipos.

Introducción a la Programación con C 47


1.16 Las bibliotecas (módulos) se importan con #include 2004/02/10-16:33

Constante Valor
M_E una aproximación del número e
M_PI una aproximación del número π
M_PI_2 una aproximación de π/2
M_PI_4 una aproximación de π/4
M_1_PI una aproximación de √
1/π
M_SQRT2 una aproximación de 2
M_LOG2E una aproximación de log2 e
M_LOG10E una aproximación 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áticas: has
de compilar con la opción -lm:

$ gcc programa.c -lm -o programa

¿Por qué? Cuando haces #include, el preprocesador introduce un fragmento de texto que
dice qué funciones pasan a estar accesibles, pero ese texto no dice qué hace cada función y cómo
lo hace (con qué instrucciones concretas). Si compilas sin -lm, el compilador se ((quejará)):

$ 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á
((quejando)) el compilador, sino otro programa del que aún no te hemos dicho nada: el enlazador
(en inglés, ((linker))). El enlazador es un programa que detecta en un programa las llamadas a
función no definidas en un programa C y localiza la definición de las funciones (ya compiladas) en
bibliotecas. El fichero math.h que incluı́mos con #define contiene la cabecera de las funciones
matemáticas, pero no su cuerpo. El cuerpo de dichas funciones, ya compilado (es decir, en
código de máquina), reside en otro fichero: /usr/lib/libm.a. ¿Para qué 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úmero 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ódigo de máquina, y ahı́ es necesario ((pegar)) (((enlazar))) el código de máquina de
las funciones matemáticas 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ódigo de máquina de nuestro programa con el código de máquina de
las bibliotecas que usamos. Con la opción -lm le indicamos al enlazador que debe resolver las
referencias indefinidas a funciones matemáticas utilizando /usr/lib/libm.a.
La opción -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

Con -L le indicamos al compilador que debe enlazar llamadas a funciones cuyo código
no se proporciona en programa.c a funciones definidas y disponibles (ya traducidas a código
de máquina) en un fichero determinado (en el ejemplo, /usr/lib/libm.a). Como la librerı́a
matemática se usa tan frecuentemente y resulta pesado escribir la ruta completa a libm.a, gcc
nos permite usar la abreviatura -lm.
El proceso completo de compilación cuando enlazamos con /usr/lib/libm.a puede repre-
sentarse gráficamente ası́:
programa.c Preprocesador Compilador Enlazador programa

/usr/lib/libm.a

48 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 34 Diseña un programa C que solicite la longitud p de los tres lados de un triángulo (a, b y c)
y muestre por pantalla su perı́metro y su área ( s(s − a)(s − b)(s − c), donde s = (a+b+c)/2.).
Compila y ejecuta el programa.
· 35 Diseña un programa C que solicite el radio r de una circunferencia y muestre por
pantalla su perı́metro (2πr) y su área (πr2 ). Utiliza la aproximación a π predefinida en la
biblioteca matemática.
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ás y
todas siguen unas reglas sintácticas diferentes. Empecemos estudiando las estructuras de control
condicionales.

1.17.1. Estructuras de control condicionales


La sentencia de selección if
La estructura de control condicional fundamental es el if . En C se escribe ası́:
if (condición) {
sentencias
}

Los paréntesis que encierran a la condición son obligatorios. Como en Python no lo son, es fácil
que te equivoques por no ponerlos. Si el bloque de sentencias consta de una sola sentencia, no
es necesario encerrarla entre llaves:
if (condición)
sentencia;

La sentencia de selección if -else


Hay una forma if -else, como en Python:
if (condición) {
sentencias_si
}
else {
sentencias_no
}

Si uno de los bloques sólo tiene una sentencia, generalmente puedes eliminar las llaves:
if (condición)
sentencia_si;
else {
sentencias_no
}

if (condición) {
sentencias_si
}
else
sentencia_no;

if (condición)
sentencia_si;
else
sentencia_no;

Introducción a la Programación con C 49


1.17 Estructuras de control 2004/02/10-16:33

Ojo: la indentación no significa nada para el compilador. La ponemos únicamente para


facilitar la lectura. Pero si la indentación no significa nada nos enfrentamos a un problema de
ambigüedad con los if anidados:
if (condición)
if (otra_condición) {
sentencias_si
}
?? ?
else { // ???
sentencias_no
}

¿A cuál de los dos if pertenece el else? ¿Hará el compilador de C una interpretación como la
que sugiere la indentación en el último fragmento o como la que sugiere este otro?:
if (condición)
if (otra_condición) {
sentencias_si
}
???
else { // ???
sentencias_no
}

C rompe la ambigüedad trabajando con esta sencilla regla: el else pertenece al if ((libre))
más cercano. Si quisiéramos expresar la primera estructura, deberı́amos añadir llaves para
determinar completamente qué bloque está dentro de qué otro:
if (condición) {
if (otra_condición) {
sentencias_si
}
}
else {
sentencias_no
}

El if externo contiene una sola sentencia (otro if ) y, por tanto, las llaves son redundantes;
pero hacen hacen evidente que el else va asociado a la condición exterior.

No hay sentencia elif : la combinación 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ón) {
sentencias_si
}
else if (condición2) {
sentencias_si2
}
else if (condición3) {
sentencias_si3
}
else {
sentencias_no
}

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 36 Diseña un programa C que pida por teclado un número entero y diga si es par o impar.
· 37 Diseña un programa que lea dos números 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.)),

50 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

((El segundo es mayor que el cuadrado del primero.)).

· 38 También en C es problemática la división por 0. Haz un programa C que resuelva la


ecuación ax + b = 0 solicitando por teclado el valor de a y b (ambos de tipo float). El programa
detectará si la ecuación no tiene solución o si tiene infinitas soluciones y, en cualquiera de los
dos casos, mostrará el pertinente aviso.
· 39 Diseña un programa que solucione ecuaciones de segundo grado. El programa detectará
y tratará por separado las siguientes situaciones:

la ecuación tiene dos soluciones reales;

la ecuación tiene una única solución real;

la ecuación no tiene solución real;

la ecuación 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á 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úmero 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ña un programa C que lea un carácter cualquiera desde el teclado, y muestre el
mensaje ((Es una MAYÚSCULA.)) cuando el carácter sea una letra mayúscula y el mensaje ((Es
una MINÚSCULA.)) cuando sea una minúscula. En cualquier otro caso, no mostrará mensaje
alguno. (Considera únicamente letras del alfabeto inglés.)
· 42 Diseña un programa que lea cinco números enteros por teclado y determine cuál de los
cuatro últimos números es más cercano al primero.
(Por ejemplo, si el usuario introduce los números 2, 6, 4, 1 y 10, el programa responderá
que el número más cercano al 2 es el 1.)
· 43 Diseña un programa que, dado un número entero, determine si éste es el doble de un
número impar.
(Ejemplo: 14 es el doble de 7, que es impar.)
.............................................................................................

La sentencia de selección switch


Hay una estructura condicional que no existe en Python: la estructura de selección múltiple. Esta
estructura permite seleccionar un bloque de sentencias en función del valor de una expresión
(tı́picamente una variable).

1 switch (expresión) {
2 case valor 1:
3 sentencias
4 break;
5 case valor 2:
6 sentencias
7 break;
8 ...
9 default:
10 sentencias
11 break;
12 }

Introducción a la Programación con C 51


1.17 Estructuras de control 2004/02/10-16:33

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ón de la opción seleccionada de un menú:
menu.c menu.c
1 #include <stdio.h>
2

3 int main(void)
4 {
5 int opcion;
6

7 printf ("1) Saluda\n");


8 printf ("2) Despı́dete\n");
9 scanf ("%d", &opcion);
10 switch (opcion) {
11 case 1:
12 printf ("Hola\n");
13 break;
14 case 2:
15 printf ("Adiós\n");
16 break;
17 default:
18 printf ("Opción no válida\n");
19 break;
20 }
21 return 0;
22 }
Aunque resulta algo más elegante esta otra versión, que hace uso de tipos enumerados:
menu 1.c menu.c
1 #include <stdio.h>
2

3 enum { Saludar =1, Despedirse };


4

5 int main(void)
6 {
7 int opcion;
8

9 printf ("1) Saluda\n");


10 printf ("2) Despı́dete\n");
11 scanf ("%d", &opcion);
12 switch (opcion) {
13 case Saludar :
14 printf ("Hola\n");
15 break;
16 case Despedirse :
17 printf ("Adiós\n");
18 break;
19 default:
20 printf ("Opción no válida\n");
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ón. 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)

52 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

6 {
7 int opcion;
8

9 printf ("1) Saluda\n");


10 printf ("2) Despı́dete\n");
11 scanf ("%d", &opcion);
12 switch (opcion) {
13 case Saludar :
14 printf ("Hola\n");
15 case Despedirse:
16 printf ("Adiós\n");
17 default:
18 printf ("Opción no válida\n");
19 }
20 return 0;
21 }
Si seleccionas la opción 1, no sale un único mensaje por pantalla, ¡salen tres: Hola, Adiós
y Opción no válida! Y si seleccionas la opción 2, ¡salen dos mensajes: Adiós y Opción no
válida! 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 última de sus lı́neas.
El compilador de C no señala 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) Saluda\n");


10 printf ("2) Despı́dete\n");
11 printf ("3) Di hola\n");
12 printf ("4) Di adiós\n");
13 scanf ("%d", &opcion);
14 switch (opcion) {
15 case Saludar :
16 case Hola:
17 printf ("Hola\n");
18 break;
19 case Despedirse:
20 case Adios:
21 printf ("Adiós\n");
22 break;
23 default:
24 printf ("Opción no válida\n");
25 break;
26 }
27 return 0;
28 }
¿Ves por qué?

1.17.2. Estructuras de control iterativas


El bucle while
El bucle while de Python se traduce casi directamente a C:
while (condición) {
sentencias
}

Introducción a la Programación con C 53


1.17 Estructuras de control 2004/02/10-16:33

Nuevamente, los paréntesis 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 = %d\n", 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ón);
El bucle do-while evalúa la condición tras cada ejecución de su bloque, ası́ que es seguro
que éste 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 negativo\n");
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 %d\n", 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: %f\n", a, b, s);
26

27 return 0;
28 }

54 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

Los bucles do-while no añaden potencia al lenguaje, pero sı́ lo dotan de mayor expresividad.
Cualquier cosa que puedas hacer con bucles do-while, puedes hacerla también con sólo bucles
while y la ayuda de alguna sentencia condicional if , pero probablemente requerirán mayor
esfuerzo por tu parte.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 44 Escribe un programa que muestre un menú en pantalla con dos opciones: ((saludar)) y
((salir)). El programa pedirá al usuario una opción y, si es válida, ejecutará su acción asociada.
Mientras no se seleccione la opción ((salir)), el menú reaparecerá y se solicitará nuevamente una
opción. Implementa el programa haciendo uso únicamente de bucles do-while.

· 45 Haz un programa que pida un número entero de teclado distinto de 1. A continuación,


el programa generará una secuencia de números enteros cuyo primer número es el que hemos
leı́do y que sigue estas reglas:

si el último número es par, el siguiente resulta de dividir a éste por la mitad;

si el último número es impar, el siguiente resulta de multiplicarlo por 3 y añadirle 1.

Todos los números se irán mostrando por pantalla conforme se vayan generando. El proceso se
repetirá hasta que el número generado sea igual a 1. Utiliza un bucle do-while.
.............................................................................................

Comparaciones y asignaciones
Un error frecuente es sustituir el operador de comparación de igualdad por el de asignación
en una estructura if o while. Analiza este par de sentencias:

a=0
?
if (a = 0) { // Lo que escribió... bien o mal?
...
}

Parece que la condición del if se evalúa a cierto, pero no es ası́: la ((comparación)) es, en
realidad, una asignación. El resultado es que a recibe el valor 0 y que ese 0, devuelto por el
operador de asignación, se considera la representación del valor ((falso)). Lo correcto hubiera
sido:

a=0
if (a == 0) { // Lo que querı́a escribir.
...
}

Aunque esta construcción es perfectamente válida, provocar la emisión de un mensaje


de error en muchos compiladores, pues suele ser fruto de un error.
Los programadores más 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á escrito una expresión
incorrecta y el compilador detendrá el proceso de traducción a código de máquina:

a=0
if (0 = a) { // Mal: error detectable por el compilador.
...
}

Introducción a la Programación con C 55


1.17 Estructuras de control 2004/02/10-16:33

El bucle for
El bucle for de Python existe en C, pero con importantes diferencias.

for (inicialización; condición; incremento) {


sentencias
}

Los paréntesis de la primera lı́nea son obligatorios. Fı́jate, además, en que los tres elementos
entre paréntesis se separan con puntos y comas.
El bucle for presenta tres componentes. Es equivalente a este fragmento de código:
inicialización;
while (condición) {
sentencias
incremento;
}

Una forma habitual de utilizar el bucle for es la que se muestra en este ejemplo, que imprime
por pantalla los números del 0 al 9 y en el que suponemos que i es de tipo int:
for (i = 0; i < 10; i++) {
printf ("%d\n", i);
}

Es equivalente, como decı́amos, a este otro fragmento de programa:


i = 0;
while (i < 10) {
printf ("%d\n", i);
i++;
}

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 46 Implementa el programa de cálculo de xn (para x y n entero) con un bucle for.
· 47 Implementa un programa que dado un número de tipo int, leı́do por teclado, se asegure
de que sólo contiene ceros y unos y muestre su valor en pantalla si lo interpretamos como un
número binario. Si el usuario introduce, por ejemplo, el número 1101, el programa mostrará el
valor 13. Caso de que el usuario instroduzca un número formado por números de valor diferente,
indı́ca al usuario que no puedes proporcionar el valor de su interpretación como número binario.
· 48 Haz un programa que solicite un número entero y muestre su factorial. Utiliza un entero
de tipo long long para el resultado. Debes usar un bucle for.
· 49 El número de combinaciones de n elementos tomados de m en m es:
 
m n n!
Cn = = .
m (n − m)! m!

Diseña un programa que pida el valor de n y m y calcule Cnm . (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é 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 ("%d\n", c);

56 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a 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áficos de alta resolución era común
representar gráficas de funciones con el terminal de caracteres. Por ejemplo, un periodo de la
función seno tiene este aspecto al representarse en un terminal de caracteres (cada punto es un
asterisco):
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*

Haz un programa C que muestre la función seno utilizando un bucle que recorre el periodo 2π
en 24 pasos (es decir, representándolo con 24 lı́neas).
· 52 Modifica el programa para que muestre las funciones seno (con asteriscos) y coseno (con
sumas) simultáneamente.
.............................................................................................
Hacer un bucle que recorra, por ejemplo, los números 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 ("%d\n", i);
}
aunque la forma habitual de expresar el incremento de i es esta otra:
for (i = 0; i < 10; i += 2) {
printf ("%d\n", i);
}
Un bucle que vaya de 10 a 1 en orden inverso presenta este aspecto:
for (i = 10; i > 0; i--) {
printf ("%d\n", i);
}

Introducción a la Programación con C 57


1.17 Estructuras de control 2004/02/10-16:33

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 = %10u\n", 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ón del bucle.
La variable i sólo existe en el ámbito del bucle, que es donde se usa.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 53 Diseña un programa C que muestre el valor de 2n para todo n entre 0 y un valor entero
proporcionado por teclado.
· 54 Haz un programa que pida al usuario una cantidad de euros, una tasa de interés y
un número de años y muestre por pantalla en cuánto se habrá convertido el capital inicial
transcurridos esos años si cada año se aplica la tasa de interés introducida.
Recuerda que un capital C a un interés del x por cien durante n años se convierte en
C · (1 + x/100)n .
(Prueba tu programa sabiendo que 10 000 euros al 4.5% de interés anual se convierten en
24 117.14 euros al cabo de 20 años.)
· 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á en pantalla
un menú 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 ángulo (en grados) entre ellos
8) Calcular la longitud
9) Finalizar

Tras la ejecución de cada una de las acciones del menú éste reaparecerá en pantalla, a menos
que la opción escogida sea la número 9. Si el usuario escoge una opción diferente, el programa
advertirá al usuario de su error y el menú reaparecerá.
Las opciones 4 y 5 pueden proporcionar resultados distintos en función del orden de los
operandos, ası́ que, si se escoge cualquiera de ellas, aparecerá un nuevo menú que permita
seleccionar el orden de los operandos. Por ejemplo, la opción 4 mostrará el siguiente menú:
1) Primer vector menos segundo vector
2) Segundo vector menos primer vector

Nuevamente, si el usuario se equivoca, se le advertirá del error y se le permitirá corregirlo.


La opción 8 del menú principal conducirá también a un submenú para que el usuario decida
sobre qué vector se aplica el cálculo de longitud.

58 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 1 Introducción a C

Puede que necesites que te refresquemos la memoria sobre los cálculos a realizar. Quizá la
siguiente tabla te sea de ayuda:

Operación Cálculo
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 ) x1 x2 + y1 y2 + z1 z2
Producto vectorial: (x1 , y1 , z1 ) × (x2 , y2 , z2 ) (y1 z2 − z1 y2 , z1 x2 − x1 z2 , x1 y2 − y1 x2 )
!
180 x1 x2 + y1 y2 + z1 z2
Ángulo entre (x1 , y1 , z1 ) y (x2 , y2 , z2 ) · arccos p p
π x1 + y12 + z12 x22 + y22 + z22
2

p
Longitud de (x, y, z) x2 + y 2 + z 2

Ten en cuenta que tu programa debe contemplar toda posible situación excepcional: divi-
siones por cero, raı́ces con argumento negativo, etc..
.............................................................................................

1.17.3. Sentencias para alterar el flujo iterativo


La sentencia break también está disponible en C. De hecho, ya hemos visto una aplicación suya
en la estructura de control switch. Con ella puedes, además, abortar al instante la ejecución
de un bucle cualquiera (while, do-while o for).
Otra sentencia de C que puede resultar útil es continue. Esta sentencia finaliza la iteración
actual, pero no aborta la ejecución del bucle.
Por ejemplo, cuando en un bucle while se ejecuta continue, la siguiente sentencia a ejecutar
es la condición del bucle; si ésta se cumple, se ejecutará una nueva iteración del bucle.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 56 ¿Qué 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 ("%d\n", i);
14 i++;
15 }
16

17 for (i = 0; i < 10; i++) {


18 if (i % 2 != 0)
19 continue;
20 printf ("%d\n", i);
21 }
22 return 0;
23 }

· 57 Traduce a C este programa Python.


1 car = raw_input(’Dame un carácter: ’)
2 if "a" <= car.lower () <= "z" or car == " ":
3 print "Este carácter es válido en un identificador en Python."
4 else:

Introducción a la Programación con C 59


1.17 Estructuras de control 2004/02/10-16:33

5 if not (car < "0" or "9" < car ):


6 print "Un dı́gito es válido en un identificador en Python,",
7 print "siempre que no sea el primer carácter."
8 else:
9 print "Carácter no válido 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ón: ’
6 print ’a) Calcular el diámetro.’
7 print ’b) Calcular el perı́metro.’
8 print ’c) Calcular el área.’
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ámetro 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 área es’, area
19 else:
20 print ’Sólo hay tres opciones: a, b o c. Tú has tecleado’, opcion
· 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úmero: ’))
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ándose
Pm de que m sea mayor
o igual que n. A continuación, muestra por pantalla el valor de i=n 1/i.
· 62 Escribe un programa que solicite un número entero y muestre todos los números primos
entre 1 y dicho número.
· 63 Haz un programa que calcule el máximo común divisor (mcd) de dos enteros positivos.
El mcd es el número más grande que divide exactamente a ambos números.
· 64 Haz un programa que calcule el máximo común divisor (mcd) de tres enteros positivos.
· 65 Haz un programa que vaya leyendo números y mostrándolos por pantalla hasta que el
usuario introduzca un número negativo. En ese momento, el programa acabará mostrando un
mensaje de despedida.
· 66 Haz un programa que vaya leyendo números hasta que el usuario introduzca un número
negativo. En ese momento, el programa mostrará por pantalla el número mayor de cuantos ha
visto.
.............................................................................................

60 Introducción a la Programación con C


Capı́tulo 2

Estructuras de datos en C: vectores


estáticos y registros

—Me llamo Alicia, Majestad —dijo Alicia con mucha educación; pero añadió para sus
adentros: ((¡Vaya!, en realidad no son más que un mazo de cartas. ¡No tengo por qué
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ño resulta conocido al compilar el programa y no sufre modificación alguna durante su
ejecución. 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ás complejo y delicado que manejarlas en Python. Como contrapartida, es más fácil definir
en C vectores multidimensionales (como las matrices) que en Python. En este capı́tulo nos
ocuparemos también de ellos. Estudiaremos además los registros en C, que permiten definir
nuevos tipos como agrupaciones de datos de tipos no necesariamente idénticos. Los registros de
C son conceptualmente idénticos a los que estudiamos en Python.

2.1. Vectores estáticos


Un vector (en inglés, ((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ón 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ón del vector.
C nos permite trabajar con vectores estáticos y dinámicos. En este capı́tulo nos ocupamos
únicamente de los denominados vectores estáticos, que son aquellos que tienen tamaño fijo
y conocido en tiempo de compilación. Es decir, el número de elementos del vector no puede
depender de datos que suministra el usuario: se debe hacer explı́cito mediante una expresión
que podamos evaluar examinando únicamente el texto del programa.

2.1.1. Declaración 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ás 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ón a la Programación con C 61


2.1 Vectores estáticos 2004/02/10-16:33

Sin cortes
Los vectores C son mucho más limitados que las listas Python. A los problemas relacionados
con el tamaño 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ábamos 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ón o
repetición, 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én 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ácter:
char a[80], b, c[80];

Se considera mal estilo declarar la talla de los vectores con literales de entero. Es preferible
utilizar algún identificador para la talla, pero teniendo en cuenta que éste debe corresponder a
una constante:
#define TALLA 80
...
char a[TALLA];

Esta otra declaración es incorrecta, pues usa una variable para definir la talla del vector1 :
int talla = 80;
...
!
char a[talla]; // No siempre es válido!

Puede que consideres válida esta otra declaración 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álido!

Una variable const es una variable en toda regla, aunque de ((sólo lectura)).

2.1.2. Inicialización 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ón. 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 ("%d\n", a[i]);
11 return 0;
12 }
1 Como siempre, hay excepciones: C99 permite declarar la talla de un vector con una expresión cuyo valor

sólo se conoce en tiempo de ejecución, pero sólo si el vector es una variable local a una función. Para evitar
confusiones, no haremos uso de esa caracterı́stica en este capı́tulo y lo consideraremos incorrecto.

62 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

Observa que el acceso a elementos del vector sigue la misma notación de Python: usamos
el identificador del vector seguido del ı́ndice encerrado entre corchetes. En una ejecución 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 ("%d\n", 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úmeros de Fibonacci.
Una vez calculados, el programa los mostrará por pantalla en orden inverso.
· 69 Escribe un programa C que almacene en un vector los 50 primeros números de Fibonacci.
Una vez calculados, el programa pedirá al usuario que introduzca un número y dirá si es o no
es uno de los 50 primeros números 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úmeros:

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áneamente un vector, debes indicar explı́ci-
tamente los valores del vector y, por tanto, esta aproximación sólo es factible para la inicializa-
ción de unos pocos valores.

2.1.3. Un programa de ejemplo: la criba de Eratóstenes


Vamos a ilustrar lo aprendido desarrollando un sencillo programa que calcule y muestre los
números primos menores que N, para un valor de N fijo y determinado en el propio programa.
Usaremos un método denominado la criba de Eratóstenes, uno de los algoritmos más antiguos y
que debemos a un astrónomo, geógrafo, matemático y filósofo de la antigua Grecia. El método
utiliza un vector de N valores booleanos (unos o ceros). Si la celda de ı́ndice i contiene el valor 1,

Introducción a la Programación con C 63


2.1 Vectores estáticos 2004/02/10-16:33

Cuestión de estilo: ¿constantes o literales al declarar la talla de un vector?


¿Por qué se prefiere declarar el tamaño 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ún 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ásemos modificar una o con
que modificásemos una de más para que el programa fuera erróneo. 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 ("%d\n", a[i]);
13 for (i = 0; i < 10 ; i++)
14 printf ("%d\n", 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ás que modificar sólo 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 ("%d\n", a[i]);
16 for (i = 0; i < TALLA_B ; i++)
17 printf ("%d\n", 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ás rápido y con mayor garantı́a de no cometer
errores.
¿Por qué en Python no nos preocupó esta cuestión? Recuerda que en Python no habı́a
declaración de variables, que las listas podı́an modificar su longitud durante la ejecución de
los programas y que podı́as consultar la longitud de cualquier secuencia de valores con la
función predefinida len. Python ofrece mayores facilidades al programador, pero a un doble
precio: la menor velocidad de ejecución y el mayor consumo de memoria.

64 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

Omisión de talla en declaraciones con inicialización y otro modo de incializar


También 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úmero de valores que aparecen
a la derecha del igual. Te recomendamos que, ahora que estás aprendiendo, no uses esta
forma de declarar vectores: siempre que puedas, opta por una que haga explı́cito el tamaño
del vector.
En C99 es posible inicializar sólo algunos valores del vector. La sintaxis es un poco
enrevesada. Aquı́ tienes un ejemplo en el que sólo inicializamos el primer y último 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últiplo
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últiplo 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ón 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ón */
10 criba[0] = 0;
11 for (i=1; i< N ; i++)
12 criba[i] = 1;
13

14 /* Criba de Eratóstenes */
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 ("%d\n", i);
24

25 return 0;
26 }

Observa que hemos tenido que decidir qué valor toma N, pues el vector criba debe tener un
tamaño 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ón reduce a una cuarta parte el tamaño del vector criba:

eratostenes 1.c eratostenes.c


1 #include <stdio.h>
2

Introducción a la Programación con C 65


2.1 Vectores estáticos 2004/02/10-16:33

3 #define N 100
4

5 int main(void)
6 {
7 char criba[N] ;
8 int i, j;
9

10 /* Inicialización */
11 criba[0] = 0;
12 for (i=1; i<N; i++)
13 criba[i] = 1;
14

15 /* Criba de Eratóstenes */
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 ("%d\n", i);
25

26 return 0;
27 }

Mejor ası́.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 70 Puedes ahorrar tiempo de ejecución 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ñar un programa que nos ayude. En una primera versión, solicitaremos las edades
de todas las personas y, a continuación, calcularemos y mostraremos por pantalla la edad
media, la desviación tı́pica, la moda y la mediana. Las fórmulas para el cálculo de la media y
la desviación tı́pica de n elementos son:
Pn
i=1 xi
x̄ = ,
rP n
n 2
i=1 (xi − x̄)
σ = ,
n
donde xi es la edad del individuo número i.2 La moda es la edad que más veces aparece (si dos
o más edades aparecen muchas veces con la máxima 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ón del vector que albergará 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

2 Hay una definición alternativa de la desviación tı́pica en la que el denominador de la fracción es 14.

66 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos 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ón de operaciones.
En el programa de la criba de Eratóstenes, por ejemplo, aún podemos reducir más 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ón:

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ón anterior.
9 int i, j;
10

11 /* Inicialización */
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óstenes */
17 for (i=2; i<N; i++)
18 if (criba[i/8] & (1 << (i%8))) // Pregunta si el bit en posición 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ón i*j.
21

22 /* Mostrar los resultados */


23 for (i=0; i<N; i++)
24 if (criba[i/8] & 1 << (i%8))
25 printf ("%d\n", i);
26

27 return 0;
28 }

¡Buf! La legibilidad deja mucho que desear. Y no sólo eso: consultar si un determinado bit
vale 1 y fijar un determinado bit a 0 resultan ser operaciones más 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ón entera, resto de división entera, desplazamiento,
negación de bits y el operador &.
¿Vale la pena reducir la memoria a una octava parte si, a cambio, el programa pierde
legibilidad y, además, resulta más lento? No hay una respuesta definitiva a esta pregunta. La
única respuesta es: depende. En según qué aplicaciones, puede resultar necesario, en otras
no. Lo que no debes hacer, al menos de momento, es obsesionarte con la optimización 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úmero %d: ", i+1);
12 scanf ("%d", &edad [i]);
13 }
14

15 return 0;
16 }

Vale la pena que te detengas a observar cómo indicamos a scanf que lea la celda de ı́ndice i en
el vector edad : usamos el operador & delante de la expresión edad [i]. Es lo que cabı́a esperar:
edad [i] es un escalar de tipo int, y ya sabes que scanf espera su dirección de memoria.
Pasamos ahora a calcular la edad media y la desviación tı́pica (no te ha de suponer dificultad
alguna con la experiencia adquirida al aprender Python):

Introducción a la Programación con C 67


2.1 Vectores estáticos 2004/02/10-16:33

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úmero %d: ", i+1);
14 scanf ("%d", &edad [i]);
15 }
16

17 /* Cálculo 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álculo 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ón de resultados */
30 printf ("Edad media : %f\n", media);
31 printf ("Desv. tı́pica: %f\n", desviacion);
32

33 return 0;
34 }

El cálculo de la moda (la edad más frecuente) resulta más problemática. ¿Cómo abordar el
cálculo? 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ños. 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úmero %d: ", i+1);
16 scanf ("%d", &edad [i]);
17 }
18

19 /* Cálculo de la media */
20 suma_edad = 0;
21 for (i=0; i<PERSONAS; i++)
22 suma_edad += edad [i];

68 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

23 media = suma_edad / (float) PERSONAS;


24

25 /* Cálculo 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álculo de la moda */
32 for (i=0; i<MAX_EDAD; i++) // Inicialización 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úsqueda 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ón de resultados */
45 printf ("Edad media : %f\n", media);
46 printf ("Desv. tı́pica: %f\n", desviacion);
47 printf ("Moda : %d\n", moda);
48

49 return 0;
50 }

Esta solución 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ás 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úmero %d: ", i+1);
15 scanf ("%d", &edad [i]);
16 }
17

18 /* Cálculo 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álculo 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álculo de la moda */

Introducción a la Programación con C 69


2.1 Vectores estáticos 2004/02/10-16:33

31 for (i=0; i<PERSONAS-1; i++) // Ordenación 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úsqueda de la serie de valores idénticos más 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ón de resultados */
54 printf ("Edad media : %f\n", media);
55 printf ("Desv. tı́pica: %f\n", desviacion);
56 printf ("Moda : %d\n", moda);
57

58 return 0;
59 }

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 71 ¿Contiene en cada instante la variable frecuencia el verdadero valor de la frecuencia de
aparición de un valor? Si no es ası́, ¿qué contiene? ¿Afecta eso al cálculo efectuado? ¿Por qué?
· 72 Esta nueva versión del programa presenta la ventaja adicional de no fijar un lı́mite
máximo a la edad de las personas. El programa resulta, ası́, de aplicación más general. ¿Son
todo ventajas? ¿Ves algún aspecto negativo? Reflexiona sobre la velocidad de ejecución del
programa comparada con la del programa que consume más memoria.
.............................................................................................
Sólo nos resta calcular la mediana. Mmmm. No hay que hacer nuevos cálculos para conocer
la mediana: gracias a que hemos ordenado el vector, la mediana es el valor que ocupa la posición
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úmero %d: ", i+1);
15 scanf ("%d", &edad [i]);
16 }
17

18 /* Cálculo de la media */
19 suma_edad = 0;

70 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

20 for (i=0; i<PERSONAS; i++)


21 suma_edad += edad [i];
22 media = suma_edad / (float) PERSONAS;
23

24 /* Cálculo 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álculo de la moda */
31 for (i=0; i<PERSONAS-1; i++) // Ordenación 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álculo de la mediana */
52 mediana = edad [PERSONAS/2]
53

54 /* Impresión de resultados */
55 printf ("Edad media : %f\n", media);
56 printf ("Desv. tı́pica: %f\n", desviacion);
57 printf ("Moda : %d\n", moda);
58 printf ("Mediana : %d\n", mediana);
59

60 return 0;
61 }

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 73 Fı́jate en la lı́nea 44 del programa y compárala con las lı́neas 44 y 45 de su versión
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áximo de 20. Cuando se introduzca un valor negativo para la edad, entende-
remos que ha finalizado la introducción 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ón a la Programación con C 71


2.1 Vectores estáticos 2004/02/10-16:33

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á 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ántos 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áxima cantidad de edades de personas que podemos
manejar, pues el número 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 , i, j, aux , suma_edad ;
9 float suma_desviacion, media, desviacion;
10 int moda, frecuencia, frecuencia_moda, mediana;

72 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos 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álculos
pertinentes:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

edad 6 18 30 18 19 19 31 1 27 66 -1 887 -55 0 391 0 -6 89 322 -2

personas 10

MAX PERSONAS 20

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álculo 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álculo 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ón a la Programación con C 73


2.1 Vectores estáticos 2004/02/10-16:33

34 /* Cálculo de la moda */
35 for (i=0; i< personas -1; i++) // Ordenación 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álculo de la mediana */
56 mediana = edad [ personas /2];
57

58 /* Impresión de resultados */
59 printf ("Edad media : %f\n", media);
60 printf ("Desv. tı́pica: %f\n", desviacion);
61 printf ("Moda : %d\n", moda);
62 printf ("Mediana : %d\n", mediana);
63 }
64 else
65 printf ("No se introdujo dato alguno.\n");
66

67 return 0;
68 }

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 74 Cuando el número 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ón alternativa de edad mediana que considera que su valor es la media de las dos
edades que ocupan las posiciones más próximas al centro.
· 75 Modifica el ejercicio anterior para que, caso de haber dos o más valores con la máxima
frecuencia de aparición, se muestren todos por pantalla al solicitar la moda.
· 76 Modifica el programa anterior para que permita efectuar cálculos con hasta 100 personas.
· 77 Modifica el programa del ejercicio anterior para que muestre, además, cuántas edades
hay entre 0 y 9 años, 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á (además de la media, desviación 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

74 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos 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á 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 último 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á 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 + p1 x + p2 x2 + p3 x3 + · · · + p10 x10 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ón a la Programación con C 75


2.1 Vectores estáticos 2004/02/10-16:33

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ás para q es innecesario cuando trabajamos
con polinomios de grado menor que 10, nuestro programa leerá los datos pidiendo en primer
lugar el grado de cada uno de los polinomios y solicitando únicamente 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érminos 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 únicamente la
inicialización 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 }

76 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos 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én menor que TALLA_POLINOMIO, ası́ que el vector s tendrá 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álculo y la


presentación 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álculo de la suma */
32 for (i=0; i<TALLA_POLINOMIO; i++)
33 s[i] = p[i] + q[i];
34

35 /* Presentación 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ón a la Programación con C 77


2.1 Vectores estáticos 2004/02/10-16:33

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á 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érmino
del polinomio no sea precedido por el carácter +.
· 84 Cuando un coeficiente es negativo, por ejemplo −1, el programa anterior muestra su
correspondiente término en pantalla ası́: + -1.000 x^1. Modifica el programa anterior para
que un término con coeficiente negativo como el del ejemplo se muestre ası́: - 1.000000 x^1.
.............................................................................................
Nos queda lo más 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é 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áximo de m(x), es decir, entre los
enteros 0 y 2*TALLA_POLINOMIO-2, se calcula ası́:
i
X
mi = pj · qi−j .
j=0

Deberemos tener cuidado de no acceder erróneamente a elementos de p o q fuera del rango de


ı́ndices válidos.
Implementemos ese cálculo:
polinomios 1.c polinomios.c
1 #include <stdio.h>
2

3 #define TALLA_POLINOMIO 11
4

78 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos 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álculo de la suma */
33 for (i=0; i<TALLA_POLINOMIO; i++)
34 s[i] = p[i] + q[i];
35

36 /* Presentación 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álculo 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ón 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ñado 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ón en los
cálculos de modo que se opere únicamente con los coeficientes de los términos de grado menor

Introducción a la Programación con C 79


2.1 Vectores estáticos 2004/02/10-16:33

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ño de los vectores siempre se determina en tiempo de compilación.
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úa
estadı́sticas de una serie de edades), necesitas alguna variable auxiliar que te permita saber
en todo momento cuántas de las celdas contienen información. Si añades un elemento, has
de incrementar tú mismo el valor de esa variable.
Ya sabes lo suficiente sobre vectores para poder hacer frente a estos ejercicios:
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 87 Diseña un programa que pida el valor de 10 números enteros distintos y los almacene en
un vector. Si se da el caso, el programa advertirá al usuario, tan pronto sea posible, si introduce
un número repetido y solicitará nuevamente el número hasta que sea diferente de todos los
anteriores. A continuación, el programa mostrará los 10 números por pantalla.
· 88 En una estación meteorológica 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ña un programa que lea los datos por teclado y muestre:
La máxima y mı́nima temperaturas de la semana.
La máxima y mı́nima temperaturas de cada dı́a.
La temperatura media de la semana.
La temperatura media de cada dı́a.
El número de dı́as en los que la temperatura media fue superior a 30 grados.
El dı́a y hora en que se registró la mayor temperatura.
· 89 La cabecera stdlib.h incluye la declaración de funciones para generar números aleato-
rios. La función rand , que no tiene parámetros, devuelve un entero positivo aleatorio. Si deseas
generar números 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úmeros aleatorios es siempre
la misma, ası́ que acabas obteniendo la misma secuencia de números aleatorios. Puedes cambiar
la semilla del generador de números aleatorios pasándole a la función srand un número entero
sin signo.
Usa el generador de números aleatorios para inicializar un vector de 10 elementos con
números enteros entre 0 y 4. Muestra por pantalla el resultado. Detecta y muestra, a con-
tinuación, el tamaño de la sucesión más larga de números consecutivos iguales.
(Ejemplo: si los números generados son 0 4 3 3 2 1 3 2 2 2, el tramo más largo formado
por números iguales es de talla 3 (los tres doses al final de la secuencia), ası́ que por pantalla
aparecerá el valor 3.)
· 90 Modifica el ejercicio anterior para que trabaje con un vector de 100 elementos.
· 91 Genera un vector con 20 números aleatorios entre 0 y 100 y muestra por pantalla el
vector resultante y la secuencia de números crecientes consecutivos más larga.
(Ejemplo: la secuencia 1 33 73 85 87 93 99 es la secuencia creciente más larga en la serie
de números 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álculo de la longitud de la secuencia
más larga sobre diferentes secuencias aleatorias (ver ejercicio anterior) y que muestre la longitud
media y desviación tı́pica de dichas secuencias.
· 93 Genera 100 números aleatorios entre 0 y 1000 y almacénalos en un vector. Determina
a continuación qué números aparecen más de una vez.

80 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

· 94 Genera 100 números aleatorios entre 0 y 10 y almacénalos en un vector. Determina a


continuación cuál es el número que aparece más veces.

· 95 Diseña un programa C que almacene en un vector los 100 primeros números primos.

· 96 Diseña un programa C que lea y almacene en un vector 10 números enteros asegurándose


de que sean positivos. A continuación, el programa pedirá que se introduzca una serie de números
enteros y nos dirá si cada uno de ellos está o no en el vector. El programa finaliza cuando el
usuario introduce un número negativo.

· 97 Diseña un programa C que lea y almacene en un vector 10 números enteros asegurándose


de que sean positivos. A continuación, el programa pedirá que se introduzca una serie de números
enteros y nos dirá si cada uno de ellos está o no en el vector. El programa finaliza cuando el
usuario introduce un número negativo.
Debes ordenar por el método de la burbuja el vector de 10 elementos tan pronto se conocen
sus valores. Cuando debas averiguar si un número está o no en el vector, utiliza el algoritmo de
búsqueda dicotómica.
.............................................................................................

2.1.6. Disposición de los vectores en memoria


Es importante que conozcas bien cómo se disponen los vectores en memoria. Cuando se encuen-
tra esta declaración 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á 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ón de un elemento de a (en bytes): %d\n", sizeof (a[0]));


10 printf ("Ocupación de a (en bytes): %d\n", sizeof (a));
11 return 0;
12 }

El resultado de ejecutarlo es éste:

Ocupación de un elemento de a (en bytes): 4


Ocupación de a (en bytes): 20

Cada byte de la memoria tiene una dirección. Si, pongamos por caso, el vector a empieza
en la dirección 1000, a[0] se almacena en los bytes 1000–1003, a[1] en los bytes 1004–1007, y
ası́ sucesivamente. El último elemento, a[4], ocupará los bytes 1016–1019:

996:
1000: a[0]
1004: a[1]
1008: a[2]
1012: a[3]
1016: a[4]
1020:

Introducción a la Programación con C 81


2.1 Vectores estáticos 2004/02/10-16:33

Big-endian y little-endian
Lo bueno de los estándares es. . . que hay muchos donde elegir. No hay forma de ponerse de
acuerdo. Muchos ordenadores almacenan los números enteros de más de 8 bits disponiendo
los bits más significativos en la dirección de memoria más baja y otros, en la más alta. Los
primeros se dice que siguen la codificación ((big-endian)) y los segundos, ((little-endian)).
Pongamos un ejemplo. El número 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ón
1000. En un ordenador ((big-endian)), se dispondrı́an en memoria ası́ (te indicamos bajo cada
byte su dirección 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ás acostumbrado a verlos: con el byte más significativo a la izquierda.
La diferente codificación de unas y otras plataformas plantea serios problemas a la hora
de intercambiar información en ficheros binarios, es decir, ficheros que contienen volcados
de la información en memoria. Nos detendremos nuevamente sobre esta cuestión 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ón 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ón de memoria de una variable. Puedes aplicar el operador & a
un elemento del vector. Por ejemplo, &a[2] es la dirección de memoria en la que empieza a[2],
es decir, la dirección 1008 en el ejemplo.
Veamos qué dirección 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ón de a[%d]: %u\n", 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ú mismo, pues el vector puede estar en un lugar cualquiera de la
memoria):

Dirección de a[0]: 3221222640


Dirección de a[1]: 3221222644
Dirección de a[2]: 3221222648

82 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

Dirección de a[3]: 3221222652


Dirección de a[4]: 3221222656

¿Ves? Cada dirección de memoria de una celda de a se diferencia de la siguiente en 4


unidades.
Recuerda que la función de lectura de datos por teclado scanf modifica el valor de una
variable cuya dirección de memoria se le suministra. Para depositar en la zona de memoria de
la variable el nuevo valor necesita conocer la dirección de memoria. Por esa razón 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ón
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é problema presenta esta otra versión 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ón de a[%d]: %u\n", i, (unsigned int) &a[i]);
11

12 printf ("Dirección de a: %u\n", (unsigned int) a);


13 return 0;
14 }

He aquı́ el resultado de ejecutarlo:

Introducción a la Programación con C 83


2.1 Vectores estáticos 2004/02/10-16:33

Dirección de a[0]: 3221222640


Dirección de a[1]: 3221222644
Dirección de a[2]: 3221222648
Dirección de a[3]: 3221222652
Dirección de a[4]: 3221222656
Dirección de a: 3221222640

Observa que la dirección de memoria de las lı́neas primera y última es la misma. En conse-
cuencia, esta lı́nea:

1 printf ("Dirección de a: %u\n", (unsigned int) &a[0]);

es equivalente a esta otra:

1 printf ("Dirección de a: %u\n", (unsigned int) a);

Ası́ pues, a expresa una dirección 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ón C.
Representaremos esquemáticamente los vectores de modo similar a como representábamos
las listas en Python:
0 1 2 3 4

a 0 0 0 0 0

Fı́jate en que el gráfico 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ñar programas con un nivel de abstracción 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ás
justo representar la memoria ası́:
0 1 2 3 4

a 0 0 0 0 0

Pero, por el momento, conviene que consideres válida la representación en la que a es un


puntero. Cuando estudiemos la gestión de memoria dinámica abundaremos en esta cuestión.

Recuerda que el operador & obtiene la dirección 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]
0 1 2 3 4

a 0 0 0 0 0
&a[0]

¿Cómo ((encuentra)) C la dirección de memoria de un elemento del vector cuando accedemos a


través de un ı́ndice? Muy sencillo, efectuando un cálculo consistente en sumar al puntero que
señala el principio del vector el resultado de multiplicar el ı́ndice por el tamaño de un elemento
del vector. La expresión a[2], por ejemplo, se entiende como ((accede al valor de tipo int que
empieza en la dirección a con un desplazamiento de 2 × 4 bytes)). Una sentencia de asignación
como a[2] = 0 se interpreta como ((almacena el valor 0 en el entero int que empieza en la
dirección de memoria de a más 2 × 4 bytes)).

84 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos 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ón 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ón3 :


+--------+----------------------+-------+
| Objeto | Dirección 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 |
3 Nuevamente, una advertencia: puede que obtengas un resultado diferente al ejecutar el programa en tu

ordenado. La asignación de direcciones de memoria a cada objeto de un programa es una decisión que adopta
el compilador con cierta libertad.

Introducción a la Programación con C 85


2.1 Vectores estáticos 2004/02/10-16:33

| 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á
estudiando, la segunda corresponde a la dirección 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: 3 i
3221222640: 10 w[0]
3221222644: 11 w[1]
3221222648: 12 w[2]
3221222652:
3221222656: 0 v[0]
3221222660: 1 v[1]
3221222664: 2 v[2]

Fı́jate en que las seis últimas filas de la tabla corresponden a accesos a v y w con ı́ndices
fuera de rango. Cuando tratábamos de acceder a un elemento inexistente en una lista Python,
el intérprete generaba un error de tipo (error de ı́ndice). Ante una situación similar, C no
detecta error alguno. ¿Qué hace, pues? Aplica la fórmula de indexación, sin más. Estudiemos
con calma el primer caso extraño: v[-2]. C lo interpreta como: ((acceder al valor almacenado en
la dirección 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ño de un int))). Haz el cálculo: el resultado es 3221222648. . .
¡la misma dirección de memoria que ocupa el valor de w[2]! Esa es la razón de que se muestre
el valor 12. En la ejecución del programa, v[-2] y w[2] son exactamente lo mismo. Encuentra
tú mismo una explicación para los restantes accesos ilı́citos.
¡Ojo! Que se pueda hacer no significa que sea aconsejable hacerlo. En absoluto. Es más:
debes evitar acceder a elementos con ı́ndices de vector fuera de rango. Si no conviene hacer
algo ası́, ¿por qué no comprueba C si el ı́ndice está en el rango correcto antes de acceder a los
elementos y, en caso contrario, nos señala un error? Por eficiencia. Un programa que maneje
vectores accederá a sus elementos, muy probablemente, en numerosas ocasiones. Si se ha de
comprobar si el ı́ndice está en el rango de valores válidos, cada acceso se penalizará con un
par de comparaciones y el programa se ejecutará más lentamente. C sacrifica seguridad por
velocidad, de ahı́ que tenga cierta fama (justificadı́sma) de lenguaje ((peligroso)).

2.1.8. Asignación 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 }
4 Si 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ón se puede reservar una zona de memoria distinta
para los datos.

86 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

Violación 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éndonos del rango de
indexación válido de otro, lo cual ha producido resultados desconcertantes.
Pero podrı́a habernos ido aún 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ón durante la
ejecución del programa: intentar escribir en una zona de memoria que no ha sido asignada
a nuestro proceso dispara, en Unix, una señal de ((violación de segmento)) (segmentation
violation) que provoca la inmediata finalización de la ejecución 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ón de segmento

El programa ha finalizado abruptamente al ejecutar la asignación de la lı́nea 7.


Estos errores en la gestión de memoria se manifiestan de formas muy variadas: pue-
den producir resultados extraños, finalizar la ejecución 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ón 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ón de segmento))). Microsoft Windows no tiene la precaución 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ón de vectores obliga a estar siempre pendiente de no
rebasar la zona de memoria reservada.

Si compilas el programa, obtendrás un error en la lı́nea 8 que te impedirá 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ón era que antes de ejecutar la lı́nea 8, la memoria presentara este aspecto:

0 1 2 3 4 5 6 7 8 9

original 1 2 3 4 5 6 7 8 9 10
0 1 2 3 4 5 6 7 8 9

copia

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ón a la Programación con C 87


2.1 Vectores estáticos 2004/02/10-16:33

0 1 2 3 4 5 6 7 8 9

original 1 2 3 4 5 6 7 8 9 10
0 1 2 3 4 5 6 7 8 9

copia 1 2 3 4 5 6 7 8 9 10

2. o conseguir que, como en Python, copia apunte al mismo lugar que original :

0 1 2 3 4 5 6 7 8 9

original 1 2 3 4 5 6 7 8 9 10
0 1 2 3 4 5 6 7 8 9

copia

Pero no ocurre ninguna de las dos cosas: el identificador de un vector estático se considera un
puntero inmutable. Siempre apunta a la misma dirección de memoria. No puedes asignar un
vector a otro porque eso significarı́a cambiar el valor de su dirección. (Observa, además, 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ón de vectores


En Python podı́amos comparar listas. Por ejemplo, [1,2,3] == [1,1+1,3] devolvı́a True. Ya
lo habrás 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 iguales\n");
19 else
20 printf ("No son iguales\n");

88 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

21

22 return 0;
23 }

2.2. Cadenas estáticas


Las cadenas son un tipo de datos básico 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ácter nulo. El carácter nulo tiene código ASCII 0 y podemos representarlo
tanto con el entero 0 como con el carácter ’\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áticas en C son, a diferencia de las cadenas Python, mutables. Eso significa
que puedes modificar el contenido de una cadena durante la ejecución de un programa.

2.2.1. Declaración de cadenas


Las cadenas se declaran como vectores de caracteres, ası́ que debes proporcionar el número
máximo 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ón:


char a[10] = "cadena";

Hemos declarado a como un vector de 10 caracteres y lo hemos inicializado asignándole 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ón 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ás
uno correspondiente a un carácter 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:


0 1 2 3 4 5 6 7 8 9

a c a d e n a \0

Es decir, es como si hubiésemos 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ño de una cadena:

su capacidad, que es la talla del vector de caracteres;

su longitud, que es el número 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é toda esta complicación del terminador de cadena? Lo normal al trabajar con una
variable de tipo cadena es que su longitud varı́e conforme evoluciona la ejecución del programa,
pero el tamaño de un vector es fijo. Por ejemplo, si ahora tenemos en a el texto "cadena" y
más tarde decidimos guardar en ella el texto "texto", que tiene un carácter menos, estaremos
pasando de esta situación:

Introducción a la Programación con C 89


2.2 Cadenas estáticas 2004/02/10-16:33

Una cadena de longitud uno no es un carácter


Hemos dicho en el capı́tulo anterior que una cadena de un sólo carácter, por ejemplo "y ",
no es lo mismo que un carácter, por ejemplo ’y’. Ahora puedes saber por qué: la diferencia
estriba en que "y " ocupa dos bytes, el que corresponde al carácter ’y’ y el que corresponde
al carácter nulo ’\0’, mientras que ’y’ ocupa un solo byte.
Fı́jate en esta declaración de variables:

1 char a = ’y’;
2 char b[2] = "y";

He aquı́ una representación gráfica de las variables y su contenido:

a y
0 1

b y \0

Recuerda:
Las comillas simples definen un carácter y un carácter ocupa un solo byte.
Las comillas dobles definen una cadena. Toda cadena incluye un carácter nulo invisible
al final.

0 1 2 3 4 5 6 7 8 9

a c a d e n a \0

a esta otra:
0 1 2 3 4 5 6 7 8 9

a t e x t o \0

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én 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én. Lo normal es que contengan valores arbitrarios, aunque eso no importa
mucho: el convenio de que la cadena termina en el primer carácter 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:
0 1 2 3 4 5 6 7 8 9

a t e x t o \0 a u \0 x

Por comodidad representaremos las celdas a la derecha del terminador con cajas vacı́as, pues
no importa en absoluto lo que contienen.
¿Qué ocurre si intentamos inicializar una zona de memoria reservada para sólo 10 chars con
una cadena de longitud mayor que 9?
!
char a[10] = "supercalifragilisticoespialidoso"; // Mal!

Estaremos cometiendo un gravı́simo error de programación 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.
0 1 2 3 4 5 6 7 8 9

a s u p e r c a l i f 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á vetada, con el consiguiente aborto de la ejecución del programa.

90 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

Como resulta que en una variable con capacidad para, por ejemplo, 80 caracteres sólo
caben realmente 79 caracteres aparte del nulo, adoptaremos una curiosa práctica al declarar
variables de cadena que nos permitirá almacenar los 80 caracteres (además del nulo) sin crear
una constante confusión con respecto al número 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ás 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ático es leer cadenas. La función scanf presenta
una seria limitación: sólo puede leer ((palabras)), no ((frases)). Ello nos obligará a presentar una
nueva función (gets). . . que se lleva fatal con scanf .

Salida con printf


Empecemos por considerar la función 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ón 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ón a la Programación con C 91


2.2 Cadenas estáticas 2004/02/10-16:33

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ácter a carácter? Podemos hacerlo llamando a printf
sobre cada uno de los caracteres, pero recuerda que la marca de formato asociada a un carácter
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 ("%c\n", cadena[i]);
13 i++;
14 }
15

16 return 0;
17 }

Este es el resultado de la ejecución:


u
n
a

c
a
d
e
n
a

Entrada con scanf


Poco más hay que contar acerca de printf . La función 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 %s\n", 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ácter & del identificador al usar scanf . ¿Por qué? Porque scanf espera

92 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

una dirección de memoria y el identificador, por la dualidad vector-puntero, ¡es una dirección
de memoria!
Recuerda: cadena[0] es un char, pero cadena, sin más, es la dirección 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álida esta otra forma de leer una cadena? Pruébala 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ón 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á un error de ejecución.
¿Y por qué printf no muestra por pantalla una simple dirección 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ón de memoria. La explicación es que la marca %s es interpretada por printf como
((me pasan una dirección de memoria en la que empieza una cadena, ası́ que he de mostrar su
contenido carácter a carácter hasta encontrar un carácter nulo)).

Lectura con gets


Hay un problema práctico con scanf : sólo 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ólo se muestra la primera:


una frase
La cadena leı́da es una.

¿Qué ha ocurrido con los restantes caracteres tecleados? ¡Están 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úsculo.

Introducción a la Programación con C 93


2.2 Cadenas estáticas 2004/02/10-16:33

¿Cómo leer, pues, una frase completa? No hay forma sencilla de hacerlo con scanf . Tendre-
mos que recurrir a una función diferente. La función 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ón de memoria que se indique como argumento y se añade 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 %s\n", 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ás caracteres de los que caben en una zona de memoria reservada para una cadena
ha dado lugar a una técnica de cracking muy común: el overflow exploit (que significa
((aprovechamiento del desbordamiento))), también 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ásica es la siguiente. Si c es una variable local a una función (en el siguiente capı́tulo
veremos cómo), reside en una zona de memoria especial: la pila. Podemos desbordar la zona
de memoria reservada para la cadena c escribiendo un texto más 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ón en una zona de la pila reservada
a información como la dirección de retorno de la función. El exploit se basa en asignar
a la dirección de retorno el valor de una dirección en la que habremos escrito una rutina
especial en código máquina. ¿Y cómo conseguimos introducir una rutina en código máquina
en un programa ajeno? ¡En la propia cadena que provoca el desbordamiento, codificándola
en binario! La rutina de código máquina suele ser sencilla: efectúa una simple llamada al
sistema operativo para que ejecute un intérprete de órdenes Unix. El intérprete se ejecutará
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érprete de órdenes
como root. ¡El ordenador es nuestro!
¿Y cómo 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ácter a carácter (lo veremos en el capı́tulo dedicado a ficheros), podemos definir nuestra
propia función de lectura de cadenas: una función de lectura que controle que nunca se
escribe en una zona de memoria más información de la que cabe.
Dado que gets es tan vulnerable a los overflow exploit, el compilador de C te dará un
aviso cuando la uses. No te sorprendas, pues, cuando veas un mensaje como éste: ((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ás en más de una ocasión: la lectura
alterna de cadenas y valores escalares. La mezcla de llamadas a scanf y a gets, produce efectos

94 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

curiosos que se derivan de la combinación 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ón
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ón directa que deberás aplicar siempre que tu
programa alterne la lectura de cadenas con blancos y valores escalares (algo muy frecuente). La
solución consiste en:

Si va 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 avariable auxiliar para ello),
• y extraer de ella los valores escalares que se deseaba leer con ayuda de la función
sscanf .

La función sscanf es similar a scanf (fı́jate en la ((s)) inicial), pero no obtiene información
leyéndola del teclado, sino que la extrae de una cadena.
Un ejemplo ayudará 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ás el terminador (puede resultar conveniente reservar más memoria para
ella en según qué aplicación). 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ón sscanf . Dicha función
recibe, como primer argumento, la cadena en linea; como segundo, una cadena con marcas
de formato; y como tercer parámetro, la dirección de la variable escalar en la que queremos
depositar el resultado de la lectura.
Es un proceso un tanto incómodo, pero al que tenemos que acostumbrarnos. . . de momento.

2.2.4. Asignación 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ón a la Programación con C 95


2.2 Cadenas estáticas 2004/02/10-16:33

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ás un error que te impedirá obtener un ejecutable. Recuerda:


los identificadores de vectores estáticos se consideran punteros inmutables y, a fin de cuentas,
las cadenas son vectores estáticos (más adelante aprenderemos a usar vectores dinámicos). Para
efectuar una copia de una cadena, has de hacerlo carácter a carácter.
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ólo necesitas copiar los caracteres que hay hasta el terminador, incluyéndole a él.
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 }

0 1 2 3 4 5 6 7 8 9

original c a d e n a \0
0 1 2 3 4 5 6 7 8 9

copia c a d e n a \0

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 100 ¿Qué problema presenta esta otra versión 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

96 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos 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ún 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ón del for controla si hemos llegado al terminador o no. Como el termi-
nado no llega a copiarse, lo añadimos tan pronto finaliza el bucle. Este tipo de bucles, aunque
perfectamente legales, pueden resultar desconcertantes.
El copiado de cadenas es una acción frecuente, ası́ que hay funciones predefinidas para ello,
accesibles incluyendo la cabecera string.h:
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ón
strcpy se limita a copiar carácter a carácter hasta llegar a un carácter nulo.
Tampoco está permitido asignar un literal de cadena a un vector de caracteres fuera de la
zona de declaración de variables. Es decir, este programa es incorrecto:
1 #define MAXLON 10
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ás que hacerlo con la ayuda de strcpy:

Introducción a la Programación con C 97


2.2 Cadenas estáticas 2004/02/10-16:33

Una versión más del copiado de cadenas


Considera esta otra versión 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 copia[i] = ’\0’;
12

13 return 0;
14 }

El bucle está vacı́o y la condición del bucle while es un tanto extraña. Se aprovecha de
que la asignación es una operación que devuelve un valor, ası́ que lo puede comparar con el
terminador. Y no sólo eso: el avance de i se logra con un postincremento en el mismı́simo
acceso al elemento de original . Este tipo de retruécanos 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é ser más rápidos y resultan más difı́ciles de entender (a menos que lleves mucho tiempo
programando en C).
Aquı́ tienes una versión con una condición del bucle while diferente:

i = 0;
while (copia[i] = original [i++]) ;
copia[i] = ’\0’;

¿Ves por qué funciona esta otra versión?

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ña un programa que lea una cadena y copie en otra una versión encriptada. La
encriptación convertirá cada letra (del alfabeto inglés) en la que le sigue en la tabla ASCII
(excepto en el caso de las letras ((z)) y ((Z)), que serán sustituidas por ((a)) y ((A)), respectivamente.)
No uses la función strcpy.
· 102 Diseña un programa que lea una cadena que posiblemente contenga letras mayúsculas
y copie en otra una versión de la misma cuyas letras sean todas minúsculas. No uses la función
strcpy.
· 103 Diseña un programa que lea una cadena que posiblemente contenga letras mayúsculas
y copie en otra una versión de la misma cuyas letras sean todas minúsculas. Usa la función
strcpy para obtener un duplicado de la cadena y, después, recorre la copia para ir sustituyendo
en ella las letras mayúsculas por sus correspondientes minúsculas.
.............................................................................................

98 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

Copias (más) 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ón de strcpy que copia la
cadena, pero con un lı́mite al número máximo 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ás caracteres de los que
puede almacenar la cadena destino, la copia es imperfecta: no acabará en ’\0’. De todos
modos, puedes encargarte tú mismo de terminar la cadena en el último carácter, 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 }

2.2.5. Longitud de una cadena


El convenio de terminar una cadena con el carácter nulo permite conocer fácilmente 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áx. %d cars.): ", MAXLON);


11 gets(a);
12 i = 0;
13 while (a[i] != ’\0’)
14 i++;
15 printf ("Longitud de la cadena: %d\n", i);
16

Introducción a la Programación con C 99


2.2 Cadenas estáticas 2004/02/10-16:33

17 return 0;
18 }

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áx. %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: %d\n", i-1 );
15

16 return 0;
17 }

El operador de postincremento permite aumentar en uno el valor de i justo después 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ás.
Es más, ni siquiera es necesario efectuar comparación 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ógico. El bucle itera, pues, hasta llegar a un valor falso, es decir, a un terminador.

Algunos problemas con el operador de autoincremento


¿Qué 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úa primero la parte derecha de la asignación, ası́ que i pasa a valer 2 y se
asigna ese valor en a[2].
Se evalúa primero la asignación, con lo que i pasa a se asigna el valor 1 en a[1] y,
después, se incrementa el valor de i, que pasa a valer 2.
¿Qué hace C? No se sabe. La especificación del lenguaje estándar indica que el resultado
está indefinido. Cada compilador elige qué hacer, ası́ que ese tipo de sentencias pueden dar
problemas de portabilidad. Conviene, pues, evitarlas.

Calcular la longitud de una cadena es una operación frecuentemente utilizada, ası́ que está
predefinida en la biblioteca de tratamiento de cadenas. Si incluı́mos la cabecera string.h,
podemos usar la función strlen (abreviatura de ((string length))):

100 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

while o for
Los bucles while pueden sustituirse muchas veces por bucles for equivalentes, bastante
más 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áx. %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: %d\n", i );
14

15 return 0;
16 }

También aquı́ es superflua la comparación:

for (i=0; a[i]; i++) ;

Todas las versiones del programa que hemos presentado son equivalentes. Escoger una
u otra es cuestión de estilo.

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 l;
10

11 printf ("Introduce una cadena (máx. %d cars.): ", MAXLON);


12 gets(a);
13 l = strlen(a);
14 printf ("Longitud de la cadena: %d\n", l);
15

16 return 0;
17 }

Has de ser consciente de qué 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ás cuanto más 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ón cuenta las vocales minúsculas 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

Introducción a la Programación con C 101


2.2 Cadenas estáticas 2004/02/10-16:33

11 printf ("Introduce una cadena (máx. %d cars.): ", MAXLON);


12 gets(a);
13 contador = 0;
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úsculas: %d\n", contador );
18

19 return 0;
20 }
Pero tiene un problema de eficiencia. Con cada iteración 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 strlen 60 veces para efectuar la comparación, y para cada llamada,
strlen tardará unos 60 pasos en devolver lo mismo: el valor 60. Esta nueva versión 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áx. %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úsculas: %d\n", contador );
19

20 return 0;
21 }

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 104 Diseña un programa que lea una cadena y la invierta.
· 105 Diseña un programa que lea una palabra y determine si es o no es palı́ndromo.
· 106 Diseña 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ón 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ácter de la
primera cadena en el que empieza, por primera vez, la segunda cadena. Si la segunda cadena
no está contenida en la primera, el programa nos lo hará saber.
(Ejemplo: si la primera cadena es "un ejercicio de ejemplo" y la segunda es "eje", el
programa mostrará el valor 3.)
· 108 Escribe un programa C que lea dos cadenas y muestre el ı́ndice del carácter de la
primera cadena en el que empieza por última vez una aparición de la segunda cadena. Si la
segunda cadena no está contenida en la primera, el programa nos lo hará saber.
(Ejemplo: si la primera cadena es "un ejercicio de ejemplo" y la segunda es "eje", el
programa mostrará 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á temporalmente en una cadena sus iniciales (las letras
con mayúsculas) separadas por puntos y espacios en blanco y mostrará el resultado en pantalla.
El programa finalizará cuando el usuario escriba una lı́nea en blanco.

102 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

· 111 Diseña 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úmero en base n. El valor de n debe
estar comprendido entre 2 y 16. Si la cadena a contiene un carácter que no corresponde a un
dı́gito en base n, notificará el error y no efectuará cálculo alguno.
Ejemplos:
si a es "ff" y n es 16, se mostrará el valor 255;
si a es "f0" y n es 15, se notificará un error: ((f no es un dı́gito en base 15));
si a es "1111" y n es 2, se mostrará el valor 15.
· 112 Diseña un programa C que lea una lı́nea y muestre por pantalla el número de palabras
que hay en ella.
.............................................................................................

2.2.6. Concatenación
Python permitı́a concatenar cadenas con el operador +. En C no puedes usar + para concatenar
cadenas. Una posibilidad es que las concatenes tú 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áx. %d cars.): ", MAXLON); gets(a);


12 printf ("Introduce otro texto (máx. %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ón de ambos: %s", a);
20

21 return 0;
22 }
Pero es mejor usar la función 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áx. %d cars.): ", MAXLON);


11 gets(a);
12 printf ("Introduce otro texto (máx. %d cars.): ", MAXLON);
13 gets(b);
14 strcat(a, b) ; // Equivale a la asignación Python a = a + b
15 printf ("Concatenación de ambos: %s", a);
16

17 return 0;
18 }

Introducción a la Programación con C 103


2.2 Cadenas estáticas 2004/02/10-16:33

Si quieres dejar el resultado de la concatenación en una variable distinta, deberás 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áx. %d cars.): ", MAXLON);


11 gets(a);
12 printf ("Introduce otro texto (máx. %d cars.): ", MAXLON);
13 gets(b);
14 strcpy(c, a) ; // Ésta seguida de...
15 strcat(c, b) ; // ... ésta equivale a la sentencia Python c = a + b
16 printf ("Concatenación de ambos: %s", c );
17

18 return 0;
19 }
Recuerda que es responsabilidad del programador asegurarse de que la cadena que recibe la
concatenación dispone de capacidad suficiente para almacenar la cadena resultante.
Por cierto, el operador de repetición de cadenas que encontrábamos en Python (operador
*) no está disponible en C ni hay función predefinida que lo proporcione.

Un carácter no es una cadena


Un error frecuente es intentar añadir un carácter a una cadena con strcat o asignárselo como
único carácter 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ácter.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 113 Escribe un programa C que lea el nombre y los dos apellidos de una persona en tres
cadenas. A continuación, el programa formará una sóla 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ón y lo mues-
tre por pantalla conjugado en presente de indicativo. Por ejemplo, si lee el texto programar,
mostrará por pantalla:
yo programo
tú programas
él programa
nosotros programamos
vosotros programáis
ellos programan
.............................................................................................

2.2.7. Comparación de cadenas


Tampoco los operadores de comparación (==, !=, <, <=, >, >=) funcionan con cadenas. Existe, no
obstante, una función de string.h que permite paliar esta carencia de C: strcmp (abreviatura

104 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

de ((string comparison))). La función 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ón:
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ético, y mayor que va detrás.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 115 Diseña 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é valor devolverá 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á
prohibido que utilices la función strcmp.
.............................................................................................

2.2.8. Funciones útiles para manejar caracteres


No sólo string.h contiene funciones útiles para el tratamiento de cadenas. En ctype.h en-
contrarás unas funciones que permiten hacer cómodamente preguntas acerca de los caracteres,
como si son mayúsculas, minúsculas, dı́gitos, etc:
isalnum(carácter ): devuelve cierto (un entero cualquiera distinto de cero) si carácter es
una letra o dı́gito, y falso (el valor entero 0) en caso contrario,
isalpha(carácter ): devuelve cierto si carácter es una letra, y falso en caso contrario,
isblank (carácter ): devuelve cierto si carácter es un espacio en blanco o un tabulador,
isdigit(carácter ) devuelve cierto si carácter es un dı́gito, y falso en caso contrario,
isspace(carácter ): devuelve cierto si carácter es un espacio en blanco, un salto de lı́nea,
un retorno de carro, un tabulador, etc., y falso en caso contrario,
islower (carácter ): devuelve cierto si carácter es una letra minúscula, y falso en caso
contrario,
isupper (carácter ): devuelve cierto si carácter es una letra mayúscula, y falso en caso
contrario.
También en ctype.h encontrarás un par de funciones útiles para convertir caracteres de minúscula
a mayúscula y viceversa:
toupper (carácter ): devuelve la mayúscula asociada a carácter , si la tiene; si no, devuelve
el mismo carácter,
tolower (carácter ): devuelve la minúscula asociada a carácter , si la tiene; si no, devuelve
el mismo carácter.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 118 ¿Qué 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ón a la Programación con C 105


2.2 Cadenas estáticas 2004/02/10-16:33

9 printf ("Es una letra\n");


10 else
11 printf ("No es una letra\n");
12

13 return 0;
14 }
.............................................................................................

2.2.9. Escritura en cadenas: sprintf


Hay una función que puede simplificar notablemente la creación de cadenas cuyo contenido se
debe calcular a partir de uno o más valores: sprintf , disponible incluyendo la cabecera stdio.h
(se trata, en cierto modo, de la operación complementaria de sscanf ). La función 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 ("%s\n", c);
13

14 return 0;
15 }

Si ejecutas el programa aparecerá 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ás legible y no cuesta mucho aprender a
usarla, pues ya sabemos usar printf . No olvides que tú eres responsable de que la información
que se almacena en c quepa.
En Python hay una acción análoga al sprintf de C: la asignación 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ón análoga a sprintf en C.
5 print c

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 119 ¿Qué 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ón, el programa
creará y mostrará una nueva cadena con los dos apellidos y el nombre (separado de los apellidos
por una coma). Por ejemplo, Juan Pérez López dará lugar a la cadena "Pérez López, Juan".
.............................................................................................

106 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

2.2.10. Un programa de ejemplo


Vamos a implementar un programa que lee por pantalla 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ólo espacio en blanco. Si, por ejemplo,
el programa lee la cadena "una cadena con blancos ", mostrará 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áx. %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 %s\n", 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ólo contenga sus letras
minúsculas o mayúsculas en el mismo orden con que aparecen en la frase.
· 123 Haz un programa que lea una frase y construya una cadena que sólo contenga sus
letras minúsculas o mayúsculas en el mismo orden con que aparecen en la frase, pero sin repetir
ninguna.
· 124 Lee un texto por teclado (con un máximo de 1000 caracteres) y muestra por pantalla
la frecuencia de aparición de cada una de las letras del alfabeto (considera únicamente letras
del alfabeto inglés), sin distinguir entre letras mayúsculas y minúsculas (una aparición 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ás de una dimensión muy fácilmente:
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úmeros 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ón a la Programación con C 107


2.3 Vectores multidimensionales 2004/02/10-16:33

La inicialización de los vectores multidimensionales necesita tantos bucles anidados como


dimensiones tengan éstos:
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én 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ón de los vectores multidimensionales en memoria


Cuando el compilador de C detecta la declaración de un vector multidimensional, reserva tantas
posiciones contiguas de memoria como sea preciso para albergar todas sus celdas.
Por ejemplo, ante la declaración int a[3][3], C reserva 9 celdas de 4 bytes, es decir, 36
bytes. He aquı́ como se disponen las celdas en memoria, suponiendo que la zona de memoria
asignada empieza en la dirección 1000:

996:
1000: a[0][0]
1004: a[0][1]
1008: a[0][2]
1012: a[1][0]
1016: a[1][1]
1020: a[1][2]
1024: a[2][0]
1028: a[2][1]
1032: a[2][2]
1036:

Cuando accedemos a un elemento a[i][j], C sabe a qué celda de memoria acceder sumando
a la dirección de a el valor (i*3+j)*4 (el 4 es el tamaño de un int y el 3 e sel número de
columnas).
Aun siendo conscientes de cómo representa C la memoria, nosotros trabajaremos con una
representación de una matriz de 3 × 3 como ésta:
0 1 2
0
a
1

Como puedes ver, lo relevante es que a es asimilable a un puntero a la zona de memoria en la


que están dispuestos los elementos de la matriz.

108 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 125 Este programa es incorrecto. ¿Por qué? Aun siendo incorrecto, produce cierta salida
por pantalla. ¿Qué 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 ("%d\n", a[0][j]);
16

17 return 0;
18 }
.............................................................................................

2.3.2. Un ejemplo: cálculo matricial


Para ilustrar el manejo de vectores multidimensionales construiremos ahora un programa que
lee de teclado dos matrices de números en coma flotante y muestra por pantalla su suma y su
producto. Las matrices leı́das serán de 3 × 3 y se denominarán a y b. El resultado de la suma
se almacenará 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álculo 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álculo del producto */

Introducción a la Programación con C 109


2.3 Vectores multidimensionales 2004/02/10-16:33

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ón del resultado de la suma */


37 printf ("Suma\n");
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ón del resultado del producto */


45 printf ("Producto\n");
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ún no sabemos definir nuestras propias funciones. En el próximo capı́tulo volveremos a ver
este programa y lo modificaremos para que use funciones definidas por nosotros.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 126 En una estación meteorológica 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ña un programa que lea los datos por teclado
y muestre:
La máxima y mı́nima temperaturas de la semana.
La máxima y mı́nima temperaturas de cada dı́a.
La temperatura media de la semana.
La temperatura media de cada dı́a.
El número de dı́as en los que la temperatura media fue superior a 30 grados.
· 127 Representamos diez ciudades con números del 0 al 9. Cuando hay carretera que une
directamente a dos ciudades i y j, almacenamos su distancia en kilómetros 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álido (las dos ciudades de todo
par consecutivo están unidas por un tramo de carretera) y, en tal caso, devuelve el número de
kilómetros del trayecto. Si el trayecto no es válido, indı́calo con un mensaje por pantalla.
La matriz de distancias deberás inicializarla explı́citamente al declararla. El vector con el
recorrido de ciudades deberás leerlo de teclado.
· 128 Diseña 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
4 [ -2.00 -56.20 +3.30 +2.00 +1.00] -> -51.90

110 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

.............................................................................................
El programa que hemos presentado adolece de un serio inconveniente si nuestro objetivo era
construir un programa ((general)) para multiplicar matrices: sólo puede trabajar con matrices de
TALLA × TALLA, o sea, de 3 × 3. ¿Y si quisiéramos trabajar con matrices de tamaños arbitrarios?
El primer problema al que nos enfrentarı́amos es el de que las matrices han de tener una talla
máxima: 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ón. Usaremos,
pues, una constante MAXTALLA con un valor razonablemente grande: pongamos 10. Ello permitirá
trabajar con matrices con un número de filas y columnas menor o igual que 10, aunque será 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úmero de filas y columnas de a se pedirá al usuario y se almacenará en sendas variables:


filas_a y columnas_a. Este gráfico ilustra su papel: la matriz a es de 10 × 10, pero sólo usamos
una parte de ella (la zona sombreada) y podemos determinar qué zona es porque filas_a y
columnas_a nos señalan hasta qué fila y columna llega la zona útil:

columnas a 3

0 1 2 3 4 5 6 7 8 9
0
a
1

filas a 5 5

Lo mismo se aplicará al número 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ón a la Programación con C 111


2.3 Vectores multidimensionales 2004/02/10-16:33

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árgate tú mismo de la lectura de b.)


La suma sólo 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álculo 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ón del resultado de la suma */


34 if (filas_a == filas_b && columnas_a == columnas_b) {
35 printf ("Suma\n");
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 n0 × m0


elementos sólo si m es igual a n0 (o sea, el número de columnas de la primera es igual al de filas
de la segunda) y que la matriz resultante es de dimensión n × m0 .
matrices 1.c matrices.c
1 #include <stdio.h>

112 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

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 a : "); scanf ("%d", &filas_b);
23 printf ("Columnas de a: "); 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álculo 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álculo 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ón del resultado de la suma */


51 if (filas_a == filas_b && columnas_a == columnas_b) {
52 printf ("Suma\n");
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ón del resultado del producto */


63 if (columnas_a == filas_b) {
64 printf ("Producto\n");

Introducción a la Programación con C 113


2.3 Vectores multidimensionales 2004/02/10-16:33

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ón 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ña un programa que lea una matriz (el usuario te indicará de
cuántas filas y columnas) y nos diga si la matriz tiene un valle o no. En caso afirmativo, nos
mostrará en pantalla las coordenadas de todos los valles, sus valores y el de sus celdas vecinas.
La matriz debe tener un número 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 candidato 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

114 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos 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á 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ácter 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 81
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: %s\n", v[i]);
14 }
15

16 return 0;
17 }

Vamos a desarrollar un programa útil que hace uso de un vector de caracteres: un pequeño
corrector ortográfico para inglés. El programa dispondrá de una lista de palabras en inglés (que
encontrarás en la página web de la asignatura, en el fichero ingles.h), solicitará al usuario que
introduzca por teclado un texto en inglés y le informará de qué palabras considera erróneas 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 últimas
lı́neas:
ingles.h ingles.h
1 #define DICCPALS 45378
2 #define MAXLONPAL 28
3 char diccionario[DICCPALS][MAXLONPAL+1] = {

Introducción a la Programación con C 115


2.3 Vectores multidimensionales 2004/02/10-16:33

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ún lo veas)


donde cada elemento es una palabra inglesa en minúsculas. La constante DICCPALS nos indica
el número de palabras que contiene el diccionario y MAXLONPAL es la longitud de la palabra más
larga (28 bytes), por lo que reservamos espacio para MAXLONPAL+1 caracteres (29 bytes: 28 más
el correspondiente al terminador nulo).
Las primeras lı́neas de nuestro programa son éstas:

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ándar del sistema (más sobre esto en el siguiente capı́tulo).
El programa empieza solicitando una cadena con gets. A continuación, la dividirá en un
nuevo vector de palabras. Supondremos que una frase no contiene más de 100 palabras y que
una palabra es una secuencia cualquiera de letras. Si el usuario introduce más de 100 palabras,
le advertiremos de que el programa sólo corrige las 100 primeras. Una vez formada la lista
de palabras de la frase, el programa buscará cada una de ellas en el diccionario. Las que no
estén, se mostrarán en pantalla precedidas del mensaje: palabra no encontrada. Vamos allá:
empezaremos por la lectura de la frase y su descomposición 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úmero 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

116 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

23 /* Descomposición 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ás 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ón de posibles errores */


45 for (i=0; i<palabras; i++)
46 printf ("%s\n", palabra[i]);
47

48 return 0;
49 }

¡Buf! Complicado, ¿no? ¡Ya estamos echando en falta el método split de Python! No nos viene
mal probar si nuestro código funciona mostrando las palabras que ha encontrado en la frase.
Por eso hemos añadido las lı́neas 45–47. Una vez hayas ejecutado el programa y comprobado
que funciona correctamente hasta este punto, comenta el bucle que muestra las palabras:
45 /* Comprobación de posibles errores */
46 // for (i=0; i<palabras; i++)
47 // printf ("%s\n", 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é?


· 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é?


· 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é?

Introducción a la Programación con C 117


2.3 Vectores multidimensionales 2004/02/10-16:33

· 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é?


.............................................................................................
Sigamos. Nos queda la búsqueda 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 última
entrada:
corrector 2.c corrector.c
.
.
.

48
?
49 /* Están 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: %s\n", 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énticas. Es, por tanto, una operación bastante costosa en tiempo.
¿Podemos reducir el número de comparaciones? ¡Claro! Como el diccionario está ordenado al-
fabéticamente, podemos abortar el recorrido cuando llegamos a una voz del diccionario posterior
(según el orden alfabético) a la que buscamos:
corrector 3.c corrector.c
.
.
.

48
?
49 /* Están 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: %s\n", palabra[i]);
61 }
62 return 0;
63 }

Con esta mejora hemos intentado reducir a la mitad el número 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

118 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

?
49 /* Están 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: %s\n", 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ún podemos hacerlo mejor. Si la lista está ordenada,
podemos efectuar una búsqueda dicotómica. La búsqueda dicotómica efectúa un número de
comparaciones reducidı́simo: ¡bastan 16 comparaciones para decidir si una palabra cualquiera
está o no en el diccionario!

corrector.c
.
.
.

97
?
98 /* Están 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: %s\n", 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áximo
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éticamente. Deberás utilizar el método de la burbuja para ordenar
el vector.

Introducción a la Programación con C 119


2.3 Vectores multidimensionales 2004/02/10-16:33

· 138 Representamos la baraja de cartas con un vector de cadenas. Los palos son "oros ",
"copas", "espadas" y "bastos". Las cartas con números entre 2 y 9 se describen con el texto
"número 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ón de las 40 cartas de la baraja. Usa bucles
siempre que puedas y compón las diferentes partes de cada descripción con strcat o sprintf . A
continuación, baraja las cartas utilizando para ello el generador de números aleatorios y muestra
el resultado por pantalla.
· 139 Diseña un programa de ayuda al diagnóstico de enfermedades. En nuestra base de
datos hemos registrado 10 enfermedades y 10 sı́ntomas:

1 char enfermedades[10][20] = { "gripe", "indigestión", "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ña un programa que pregunte al paciente si sufre cada uno de los 10 sı́ntomas y, en
función de las respuestas dadas, determine la enfermedad que padece. Si la descripción de sus
sı́ntomas no coincide exactamente con la de alguna de las enfermedades, el sistema indicará que
no se puede emitir un diagnóstico fiable.
· 140 Modifica el programa anterior para que, cuando no hay coincidencia absoluta de
sı́ntomas, muestre las tres enfermedades con sintomatologı́a más parecida. Si, por ejemplo, una
enfermedad presenta 9 coincidencias con la sintomatologı́a del paciente, el sistema mostrará el
nombre de la enfermedad y el porcentaje de confianza del diagnóstico (90%).
· 141 Vamos a implementar un programa que nos ayude a traducir texto a código Morse.
Aquı́ tienes una tabla con el código 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á una lı́nea y mostrará por pantalla su traducción a código 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án en cuenta al efectuar la traducción (la
letra Á, por ejemplo, se representará con .-) y la letra ’~
N’ se mostrará como una ’N’. Los signos
que no aparecen en la tabla (comas, admiraciones, etc.) no se traducirán, excepción hecha del
punto, que se traduce por la palabra STOP. Te conviene pasar la cadena a mayúsculas (o efectuar
esta transformación sobre la marcha), pues la tabla Morse sólo recoge las letras mayúsculas y
los dı́gitos.
Por ejemplo, la cadena "Hola, mundo." se traducirá por

.... --- .-.. .- -- ..- -. -.. --- ... - --- .--.

Debes usar un vector de cadenas para representar la tabla de traducción a Morse. El código
Morse de la letra ’A’, por ejemplo, estará accesible como una cadena en morse[’A’].
(Tal vez te sorprenda la notación morse[’A’]. Recuerda que ’A’ es el número 65, pues
el carácter ’A’ tiene ese valor ASCII. Ası́ pues, morse[’A’] y morse[65] son lo mismo. Por
cierto: el vector de cadenas morse sólo tendrá códigos para las letras mayúsculas y los dı́gitos;
recuerda inicializar el resto de componentes con la cadena vacı́a.)

120 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

· 142 Escribe un programa que lea un texto escrito en código Morse y lo traduzca al código
alfabético.
Si, por ejemplo, el programa lee por teclado esta cadena:
".... --- .-.. .- -- ..- -. -.. --- ... - --- .--."
mostrará 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és de un ı́ndice.
En ocasiones necesitarás agrupar datos de diferentes tipos y/o preferirás acceder a diferentes
elementos de un grupo de datos a través de un identificador, no de un ı́ndice. Los registros
son agrupaciones heterogéneas 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ño tı́pico de registro. Supongamos que deseamos mantener los siguien-
tes datos de una persona:
su nombre (con un máximo de 40 caracteres),
su edad (un entero),
su DNI (una cadena de 9 caracteres).
Podemos definir un registro ((persona)) antes de la aparición 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ácil olvidarse de ponerlo.

La definición 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
separando con un punto el identificador de la variable del correspondiente identificador del
campo. Por ejemplo, pepe.edad es la edad de Pepe (un entero sin signo que ocupa un byte),
juan.nombre es el nombre de Juan (una cadena), y ana.dni [9] es la letra del DNI de Ana (un
carácter).
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é hemos resaltado lo de ((en principio)), lee
el cuadro ((Alineamientos)).)
Este programa ilustra cómo acceder a los campos de un registro leyendo por teclado sus
valores y mostrando por pantalla diferentes informaciones almacenadas en él:
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];

Introducción a la Programación con C 121


2.4 Registros 2004/02/10-16:33

Alineamientos
El operador sizeof devuelve el tamaño 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ón: %d bytes\n", sizeof (struct Registro));
11 return 0;
12 }

Parece que vaya a mostrar en pantalla el mensaje ((Ocupación: 5 bytes)), pues un char
ocupa 1 byte y un int ocupa 4. Pero no es ası́:
Ocupación: 8 bytes

La razón de que ocupe más de lo previsto es la eficiencia. Los ordenadores con arqui-
tectura de 32 bits agrupan la información 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 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ón 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.

9 int edad ;
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: %s\n", ejemplo.nombre);


24 printf ("Edad leı́da : %d\n", ejemplo.edad );
25 printf ("DNI leı́do : %s\n", 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

122 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

39 printf ("%c\n", ejemplo.dni[longitud -1]);


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úsculas 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: %s\n", una.nombre);


31 printf ("Edad leı́da : %d\n", una.edad );
32 printf ("DNI leı́do : %s\n", una.dni);
33

34 printf ("Nombre copia: %s\n", copia.nombre);


35 printf ("Edad copia : %d\n", copia.edad );
36 printf ("DNI copia : %s\n", copia.dni);
37

38 return 0;
39 }

Observa que la copia se efectúa incluso cuando los elementos del registro son vectores. O
sea, copiar vectores con una mera asignación está prohibido, pero copiar registros es posible.
Un poco incoherente, ¿no?
Por otra parte, no puedes comparar registros. Este programa, por ejemplo, efectúa una copia
de un registro en otro para, a continuación, 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 ;

Introducción a la Programación con C 123


2.4 Registros 2004/02/10-16:33

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); sscanf (linea, "%d", &una.edad );
20 printf ("DNI : "); gets(una.dni);
21

22 copia = una; // Copia


23

24 if ( copia == una ) // Comparación ilegal.


25 printf ("Son iguales\n");
26 else
27 printf ("No son iguales\n");
28

29 return 0;
30 }

Pero ni siquiera es posible compilarlo. La lı́nea 24 contiene un error que el compilador señala
como ((invalid operands to binary ==)), o sea, ((operandos inválidos para la operación bina-
ria ==)). Entonces, ¿cómo podemos decidir si dos registros son iguales? Comparando 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 iguales\n");
27 else
28 printf ("No son iguales\n");
29

30 return 0;
31 }

124 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

Una razón para no comparar


Si C sabe copiar una estructura ((bit a bit)), ¿por qué no sabe compararlas ((bit a bit))?
El problema estriba en construcciones como las cadenas que son campos de un registro.
Considera esta definición:

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ólo 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én valga "Pepe" (y tenga idéntico apellido) puede tener valores
diferentes en su segundo grupo de cinco bytes. Una comparación ((bit a bit)) nos dirı́a que
los registros son diferentes.
La asignación no entraña este tipo de problema, pues la copia es ((bit a bit)). Como
mucho, resulta algo ineficiente, pues copiará hasta los bytes de valor indefinido.

Una forma de inicialización


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áticos tienen una talla fija. Cuando necesitamos un vector cuya talla varı́a o no
se conoce hasta iniciada la ejecución 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ántos elementos del vector contenı́an realmente datos. Hay algo poco elegante en esa solución:
las variables edad y personas son variables independientes, que no están relacionadas entre sı́
en el programa (salvo por el hecho de que nosotros sabemos que sı́ lo están). Una solución más
elegante pasa por crear un registro que contenga el número de personas y, en un vector, las
edades. He aquı́ el programa que ya te presentamos en su momento convenientemente modificado
según este nuevo principio de diseño:

Introducción a la Programación con C 125


2.4 Registros 2004/02/10-16:33

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úmero de edades realmente almacenadas.
9 };
10

11 int main(void)
12 {
13 struct ListaEdades personas;
14 int i, j, aux , suma_edad ;
15 float media, desviacion, suma_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álculo 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 / personas.talla ;
34

35 /* Cálculo 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álculo de la moda */
42 for (i=0; i< personas.talla -1; i++) // Ordenación 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álculo de la mediana */

126 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

63 mediana = personas.edad [personas.talla/2] ;


64

65 /* Impresión de resultados */
66 printf ("Edad media : %f\n", media);
67 printf ("Desv. tı́pica: %f\n", desviacion);
68 printf ("Moda : %d\n", moda);
69 printf ("Mediana : %d\n", mediana);
70 }
71 else
72 printf ("No se introdujo dato alguno.\n");
73

74 return 0;
75 }

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 143 Modifica el programa de cálculo con polinomios que sirvió de ejemplo en el apar-
tado 2.1.5 para representar los polinomios mediante registros. Cada registro contendrá dos
campos: el grado del polinomio y el vector con los coeficientes.
.............................................................................................

2.4.2. Un ejemplo: rectas de regresión para una serie de puntos en el plano


Hay métodos estadı́sticos que permiten obtener una recta que se ajusta de forma óptima 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álculos:
Pn Pn Pn
( i=1 xi ) · ( i=1 yi ) − n · i=1 xi yi
m = Pn 2 Pn ,
( i=1 xi ) − n · i=1 x2i
Pn Pn 2
 Pn Pn
( i=1 yi ) · i=1 xi − ( i=1 xi ) · ( i=1 xi yi )
b = Pn Pn 2 .
2
n i=1 xi − ( i=1 xi )

Las fórmulas asustan un poco, pero no contienen más que sumatorios. El programa que vamos
a escribir lee una serie de puntos (con un número máximo 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á talla 1000:


#define TALLAMAX 1000
struct Punto p[TALLAMAX];

Pero 1000 es el número máximo de puntos. El número de puntos disponibles efectivamente será
menor o igual y su valor deberá estar accesible en alguna variable. Olvidémonos del vector p:
nos conviene definir un registro en el que se almacenen vector y talla real del vector.

Introducción a la Programación con C 127


2.4 Registros 2004/02/10-16:33

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ómo podemos acceder a la información de la variable lista:

Expresión 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ántos elementos del vector contienen
información.
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á 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 último elemento con información del vec-
tor. Su tipo es float.
lista.punto.x ¡Error! Si lista.puntos es un vector, no podemos acceder al
campo x.
lista.punto.x[0] ¡Error! Si lo anterior era incorrecto, ésto lo es aún más.
lista.punto.[0].x ¡Error! ¿Qué hace un punto antes del operador de indexa-
ción?
lista[0].punto ¡Error! La variable lista no es un vector, ası́ que no puedes
aplicar el operador de indexación sobre ella.

Ahora que tenemos más claro cómo hemos modelado la información, vamos a resolver el
problema propuesto. Cada uno de los sumatorios se precalculará cuando se hayan leı́do los
puntos. De ese modo, simplificaremos significativamente las expresiones de cálculo de m y b.
Debes tener en cuenta que, aunque en las fórmulas 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;
7 };

128 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

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álculo 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álculo de m y b e impresión de resultados */


47 if (sx * sx - lista.talla * sxx == 0)
48 printf ("Indefinida\n");
49 else {
50 m = (sx * sy - lista.talla * sxy) / (sx * sx - lista.talla * sxx );
51 printf ("m = %f\n", m);
52 b = (sy * sxx - sx * sxy) / (lista.talla * sxx - sx * sx );
53 printf ("b = %f\n", b);
54 }
55

56 return 0;
57 }

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 144 Diseña un programa que lea una lista de hasta 1000 puntos por teclado y los almacene
en una variable (del tipo que tú mismo definas) llamada representantes. A continuación, irá
leyendo nuevos puntos hasta que se introduzca el punto de coordenadas (0, 0). Para cada nuevo
punto, debes encontrar cuál es el punto más próximo de los almacenados en representantes.
Calcula la distancia entre dos puntos como la distancia euclı́dea.
· 145 Deseamos efectuar cálculos con enteros positivos de hasta 1000 cifras, más de las que
puede almacenar un int (o incluso long long int). Define un registro que permita representar
números de hasta 1000 cifras con un vector en el que cada elemento es una cifra (representada
con un char). Representa el número de cifras que tiene realmente el valor almacenado con un
campo del registro. Escribe un programa que use dos variables del nuevo tipo para leer dos

Introducción a la Programación con C 129


2.4 Registros 2004/02/10-16:33

números 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ón de una coleción de CDs


Estamos en condiciones de abordar la implementación de un programa moderadamente com-
plejo: la gestión de una colección 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érprete (una cadena con, a lo sumo, 40 caracteres),
la duración (en minutos y segundos),
el año de publicación.
Definiremos un registro para almacenar los datos de un CD y otro para representar la duración,
ya que ésta 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ón, definiremos un máximo número de CDs:
1000. Eso no significa que la colección tenga 1000 discos, sino que puede tener a lo sumo 1000.
¿Y cuántos tiene en cada instante? Utilizaremos una variable para mantener el número de CDs
presente en la colección. Mejor aún: definiremos un nuevo tipo de registro que represente a la
colección entera de CDs. El nuevo tipo contendrá dos campos:
el vector de discos (con capacidad limitada a 1000 unidades),
y el número de discos en el vector.
He aquı́ la definición de la estructura y la declaración de la colección de CDs:
#define MAXDISCOS 1000

...

struct Coleccion {
struct CompactDisc cd [MAXDISCOS];
int cantidad ;
};

struct Coleccion mis_cds;

Nuestro programa permitirá efectuar las siguientes acciones:


Añadir un CD a la base de datos.
Listar toda la base de datos.
Listar los CDs de un intérprete.

130 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

Suprimir un CD dado su tı́tulo y su intérprete.


(El programa no resultará muy útil hasta que aprendamos a utilizar ficheros en C, pues al
finalizar cada ejecución se pierde toda la información 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ón de la colección. */
37 mis_cds.cantidad = 0;
38

39 /* Bucle principal: menú de opciones. */


40 do {
41 do {
42 printf ("Colección de CDs\n");
43 printf ("----------------\n");
44 printf ("1) A~nadir CD\n");
45 printf ("2) Listar todo\n");
46 printf ("3) Listar por intérprete\n");
47 printf ("4) Suprimir CD\n");
48 printf ("5) Salir\n");
49 printf ("Opción: ");
50 gets(linea); sscanf (linea, "%d", &opcion);
51 if (opcion <1 || opcion >5)
52 printf ("Opción inexistente. Debe estar entre 1 y 5\n");
53 } while (opcion <1 || opcion >5);
54

55 switch(opcion) {
56 case Anyadir : // Añadir un CD.
57 if (mis_cds.cantidad == MAXDISCOS)

Introducción a la Programación con C 131


2.4 Registros 2004/02/10-16:33

58 printf ("La base de datos está llena. Lo siento.\n");


59 else {
60 printf ("Tı́tulo: ");
61 gets(mis_cds.cd [mis_cds.cantidad ].titulo);
62 printf ("Intérprete: ");
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) %d\n", 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érprete.


84 printf ("Intérprete: "); 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) %d\n", 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érprete: "); 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ón del tipo struct Coleccion de la declara-
ción de la variable mis_cds. No es necesario. Podemos definir el tipo y declarar la variable en
una sola sentencia:

struct Coleccion {
struct CompactDisc cd [MAXDISCOS];
int cantidad ;

132 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos y registros

} mis_cds ; // Declara la variable mis_cds como de tipo struct Coleccion.

Apuntemos ahora cómo enriquecer nuestro programa de gestión de una colección de discos
compactos almacenando, además, las canciones de cada disco. Empezaremos por definir un
nuevo registro: el que modela una canción. De cada canción nos interesa el tı́tulo, el autor y la
duración:
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úmero de canciones que realmente hay.
10 };

¿Cómo leemos ahora un disco compacto? Aquı́ tienes, convenientemente modificada, la por-
ción del programa que se encarga de ello:
1 ...
2 int main(void)
3 {
4 int segundos;
5 ...
6 switch(opcion) {
7 case Anyadir : // Añadir un CD.
8 if (mis_cds.cantidad == MAXDISCOS)
9 printf ("La base de datos está llena. Lo siento.\n");
10 else {
11 printf ("Tı́tulo: ");
12 gets(mis_cds.cd [mis_cds.cantidad ].titulo);
13 printf ("Intérprete: ");
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úmero 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ón número %d: ", i);
25 gets(mis_cds.cd [mis_cds.cantidad ].cancion[i].titulo);
26 printf ("Autor de la canción número %d: ", i);
27 gets(mis_cds.cd [mis_cds.cantidad ].cancion[i].autor );
28 printf ("Minutos que dura la canción número %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 }
35

36 segundos = 0;

Introducción a la Programación con C 133


2.5 Definición de nuevos tipos de datos 2004/02/10-16:33

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ómo se calcula ahora la duración del compacto como suma de las duraciones de todas
sus canciones.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 146 Diseña un programa C que gestione una agenda telefónica. Cada entrada de la agenda
contiene el nombre de una persona y hasta 10 números de teléfono. El programa permitirá
añadir nuevas entradas a la agenda y nuevos teléfonos a una entrada ya existente. El menú
del programa permitirá, además, borrar entradas de la agenda, borrar números de teléfono
concretos de una entrada y efectuar búsquedas por las primeras letras del nombre. (Si, por
ejemplo, tu agenda contiene entradas para ((José Martı́nez)), ((Josefa Pérez)) y ((Jaime Primero)),
una búsqueda por ((Jos)) mostrará a las dos primeras personas y una búsqueda por ((J)) las
mostrará a todas.)
.............................................................................................

2.5. Definición 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úmero de canciones que realmente hay.
27 };

Hay una forma más compacta de definir un nuevo tipo a partir de un registro:

134 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 2 Estructuras de datos en C: vectores estáticos 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úmero 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áticamente, hemos utilizado iniciales mayúsculas para los nombres de
tipos de datos (definidos con typedef y struct o sólo 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ólo sirve para eliminar la molesta palabra clave struct,
también permite diseñar programas más legibles y en los que resulta más fácil 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ácter, sino un número. Si definimos un ((nuevo)) tipo,
el programa es más legible:
typedef char TipoEdad;

TipoEdad edad ;

Es más, si más adelante deseamos cambiar el tipo char por int, sólo 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ón a la Programación con C 135


2.5 Definición de nuevos tipos de datos 2004/02/10-16:33

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ático. Imagina
que en una aplicación definimos un tipo edad como un carácter 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 %hhu\n", mi_edad );
12

13 return 0;
14 }

¿Qué pasa si, posteriormente, decidimos que el tipo TipoEdad debiera ser un entero
de 32 bits? He aquı́ una versión errónea 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 %hhu\n" , mi_edad ); // Mal!
12

13 return 0;
14 }

¿Y por qué es erróneo? Porque debiéramos haber modificado además las marcas de formato
de scanf y printf : en lugar de %hhu deberı́amos usar ahora %hd.
C no es un lenguaje idóneo para este tipo de modificaciones. Otros lenguajes, como C++
soportan de forma mucho más flexible la posibilidad de cambiar tipos de datos, ya que no
obligan al programador a modificar un gran número de lı́neas del programa.

136 Introducción a la Programación con C


Capı́tulo 3

Funciones

Un momento después, Alicia atravesaba el cristal, y saltaba ágilmente a la habitación


del Espejo.

Lewis Carroll, Alicia a través del espejo.

Vamos a estudiar la definición y uso de funciones en C. El concepto es el mismo que ya estudiaste


al aprender Python: una función es un fragmento de programa parametrizado que efectúa unos
cálculos y, o devuelve un valor como resultado, o tiene efectos laterales (modificación de variables
globales o argumentos, volcado de información en pantalla, etc.), o ambas cosas. La principal
diferencia entre Python y C estriba en el paso de parámetros. En este aspecto, C presenta ciertas
limitaciones frente a Python, pero también ciertas ventajas. Entre las limitaciones tenemos la
necesidad de dar un tipo a cada parámetro 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ón (gracias
al uso de punteros).
Estudiaremos también la posibilidad de declarar y usar variables locales, y volveremos a
tratar la recursividad. Además, veremos cómo implementar nuestros propios módulos mediante
las denominadas unidades de compilación y la creación de ficheros de cabecera.
Finalmente, estudiaremos la definición y el uso de macros, una especie de ((pseudo-funciones))
que gestiona el preprocesador de C.

3.1. Definición de funciones


En C no hay una palabra reservada (como def en Python) para iniciar la definición de una
función. El aspecto de una definición de función en C es éste:
1 tipo_de_retorno identificador ( parámetros )
2 {
3 cuerpo_de_la_función
4 }

El cuerpo de la función puede contener declaraciones de variables locales (tı́picamente en sus


primeras lı́neas).
Aquı́ tienes un ejemplo de definición de función: una función que calcula el logaritmo en
base b (para b entero) de un número 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ón:
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ámonos a analizar brevemente cada uno de los componentes de la definición de una


función e identifiquémoslos en el ejemplo:

Introducción a la Programación con C 137


3.1 Definición de funciones 2004/02/10-16:33

El tipo de retorno indica de qué tipo de datos es el valor devuelto por la función como
resultado (más adelante veremos cómo definir procedimientos, es decir, funciones sin valor
de retorno). Puedes considerar esto como una limitación frente a Python: en C, cada
función devuelve valores de un único tipo. No podemos definir una función que, según
convenga, devuelva un entero, un flotante o una cadena, como hicimos en Python cuando
nos convino.
En nuestro ejemplo, la función 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ón 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ón con un identificador que ya hayas usado para una variable (u otra función).
El identificador de nuestra función 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éntesis aparece una lista de declaraciones de parámetros separadas por comas.
Cada declaración de parámetro indica tanto el tipo del mismo como su identificador1 .
Nuestra función tiene dos parámetros, 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ón debe ir encerrado entre llaves, aunque sólo conste de una sentencia.
Puede empezar por una declaración de variables locales a la que sigue una o más sentencias
C. La sentencia return permite finalizar la ejecución de la función y devolver un valor
(que debe ser del mismo tipo que el indicado como tipo de retorno). Si no hay sentencia
return, la ejecución de la función finaliza también al acabar de ejecutar la última de las
sentencias de su cuerpo, pero es un error no devolver nada con return si se ha declarado
la función como tal, y no como procedimiento.
Nuestra función de ejemplo tiene un cuerpo muy sencillo. Hay una declaración de variables
(locales) y está formado por tres sentencias, dos de asignación y una de devolución de
valor:

1 float logaritmo (float x, int b)


2 {
3 float logbase, resultado;
4

1 Eso en el caso de parámetros escalares. Los parámetros de tipo vectorial se estudiarán más adelante.

138 Introducción a la Programación con C



c 2003 Andrés 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ón de valor forma(n) parte del cuerpo y empieza(n)


con la palabra return. Una función puede incluir más de una sentencia de devolución de
valor, pero debes tener en cuenta que la ejecución de la función finaliza con la primera
ejecución 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ón logaritmo se invoca como una función 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 ("%f\n", y);
19

20 return 0;
21 }

Si ejecutamos el programa tenemos:

7.000000

Es necesario que toda función se defina en el programa antes de la primera lı́nea en que
se usa. Por esta razón, todas nuestras funciones se definen delante de la función main, que es
la función que contiene el programa principal y a la que, por tanto, no se llama desde ningún
punto del programa.2
Naturalmente, ha resultado necesario incluir la cabecera math.h en el programa, ya que
usamos la función log10. Recuerda, además, que al compilar se debe enlazar con la biblioteca
matemática, es decir, se debe usar la opción -lm de gcc.
Esta ilustración te servirá para identificar los diferentes elementos de la definición de una
función y de su invocación:
2 Nuevamente hemos de matizar una afirmación: en realidad sólo es necesario que se haya declarado el prototipo

de la función. Más adelante daremos más detalles.

Introducción a la Programación con C 139


3.1 Definición de funciones 2004/02/10-16:33

Tipo de retorno

Identificador

Parámetros formales (o simplemente parámetros)

float logaritmo (float x, int b) Cabecera


{
float logbase, resultado; Declaración de variables locales

logbase = log10(b); Cuerpo


resultado = log10(x)/logbase;
return resultado; Sentencia de devolución de valor
}
..
.
int main(void)
{
float y;
.. Identificador
.
y = logaritmo( 128.0, 2 ); Llamada, invocación o activación
..
. Argumentos o parámetros reales
¡Ah! Te hemos dicho antes que la función 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ón que reciba un int y devuelva su cuadrado.
· 148 Define una función que reciba un float y devuelva su cuadrado.
· 149 Define una función 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ón 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ólico de x es
ex − e−x
sinh = .
2
Diseña una función C que efectúe el calculo de senos hiperbólicos. (Recuerda que ex se puede
calcular con la función exp, disponible incluyendo math.h y enlazando el programa ejecutable
con la librerı́a matemática.)
· 152 Diseña una función que devuelva ((cierto)) (el valor 1) si el año 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
Ax0 + By0 + C
d= √ .
A2 + B 2
Diseña una función que reciba los valores que definen una recta y los valores que definen un
punto y devuelva la distancia del punto a la recta.

140 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

.............................................................................................
Veamos otro ejemplo de definición de función:
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ón minimo devuelve un dato de tipo int y recibe tres datos, también de tipo int. No
hay problema en que aparezca más de una sentencia return en una función. El comportamiento
de return es el mismo que estudiamos en Python: tan pronto se ejecuta, finaliza la ejecución
de la función y se devuelve el valor indicado.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 154 Define una función que, dada una letra minúscula del alfabeto inglés, devuelva su
correspondiente letra mayúscula. Si el carácter recibido como dato no es una letra minúscula,
la función la devolverá inalterada.
· 155 ¿Qué error encuentras en esta función?
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ón. Su cabecera es int main(void). ¿Qué significa void?
Significa que no hay parámetros. Pero no nos adelantemos. En este mismo capı́tulo hablaremos
de funciones sin parámetros.

3.2. Variables locales y globales


Cada función puede definir sus propias variables locales definiéndolas en su cuerpo. C permite,
además, definir variables fuera del cuerpo de cualquier función: son las variables globales.

3.2.1. Variables locales


Las variables que declaramos justo al principio del cuerpo de una función son variables P
locales.
b
Este programa, por ejemplo, declara dos variables locales para calcular el sumatorio 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ón a la Programación con C 141


3.2 Variables locales y globales 2004/02/10-16:33

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úmeros naturales: %d\n", i, sumatorio(1, i));
19 return 0;
20 }
Las variables locales i y s de sumatorio sólo ((viven)) durante las llamadas a sumatorio.
La zona en la que es visible una variable es su ámbito. Las variables locales sólo son visibles
en el cuerpo de la función en la que se declaran; ése es su ámbito.

Variables locales a bloques


El concepto de variable local no está 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ólo existe en el bloque en el que se ha declarado, es decir, en la zona sombreada.


Ese es su ámbito. La variable i tiene un ámbito que engloba al de j.
Puedes comprobar, pues, que una variable local a una función es también una variable
local a un bloque: sólo existe en el bloque que corresponde al cuerpo de la función.
Como ya te dijimos en un cuadro del capı́tulo 1, C99 permite declarar variables de ı́ndice
de bucle de usar y tirar. Su ámbito se limita al bucle. Aquı́ tienes un ejemplo en el que
hemos sombreado el ámbito 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ña una función que calcule el factorial de un entero n.

142 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

· 157 Diseña una función 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ón ex puede aproximarse con el desarrollo de Taylor:
x2 x3 x4
ex ≈ 1 + x + + + + ···
2! 3! 4!
Diseña una función que aproxime el valor de ex usando n términos del desarrollo de Taylor,
siendo n un número entero positivo. (Puedes usar, si te conviene, la función de exponenciación
del último ejercicio para calcular los distintos valores de xi , aunque hay formas más eficientes
de calcular x/1!, x2 /2!, x3 /3!, . . . , ¿sabes cómo? Plantéate cómo generar un término de la forma
xi /i! a partir de un término de la forma xi−1 /(i − 1)!.)
· 159 El valor de la función coseno puede aproximarse con el desarrollo de Taylor:
x2 x4 x6
cos(x) ≈ 1 − + − + ···
2! 4! 6!
Diseña una función que aproxime el coseno de un valor x usando n términos del desarrollo de
Taylor, siendo n un número entero positivo.
· 160 Diseña una función que diga si un número es perfecto o no. Si el número es perfecto,
devolverá ((cierto)) (el valor 1) y si no, devolverá ((falso)) (el valor 0). Un número es perfecto si
es igual a la suma de todos sus divisores (excepto él mismo).
· 161 Diseña una función que diga si un número entero es o no es capicúa.
.............................................................................................

3.2.2. Variables globales


Las variables globales se declaran fuera del cuerpo de cualquier función y son accesibles desde
cualquier punto del programa posterior a su declaración. 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 ("%d\n", doble()); // Ojo: el valor mostrado corresponde a la i global.
17

18 return 0;
19 }

Fı́jate en la pérdida 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ámetros en funciones, llegan a resultar muy cómodas y es fácil 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ándo resulta conveniente usar una variable global y cuándo conviene
suministrar información a funciones mediante parámetros.
Como estudiante te pueden parecer un recurso cómodo para evitar suministrar información
a las funciones mediante parámetros. Ese pequeño beneficio inmediato es, creenos, un lastre a

Introducción a la Programación con C 143


3.3 Funciones sin parámetros 2004/02/10-16:33

medio y largo plazo: aumentará la probabilidad de que cometas errores al intentar acceder o
modificar una variable y las funciones que definas en un programa serán difı́cilmente reutilizables
en otros. Estás 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é 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ámetros


Puedes definir una función sin parámetros dejando la palabra void como contenido de la lista
de parámetros. Esta función definida por nosotros, por ejemplo, utiliza la función rand de
stdlib.h para devolver un número aleatorio entre 1 y 6:

int dado (void)


{
return rand () % 6 + 1;
}

144 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

Para llamar a la función dado hemos de añadir un par de paréntesis a la derecha del iden-
tificador, aunque no tenga parámetros.
Ya te habı́amos anticipado que la función main es una función sin parámetros 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 ("%d\n", dado() );
14 return 0;
15 }

((Calidad)) de los números aleatorios


La función rand está pobremente implementada en muchas máquinas y genera patrones
repetitivos que hacen poco aleatoria la secuencia de números generada. Este problema se
agudiza si observamos los bits menos significativos de los números generados. . . ¡y eso es,
precisamente, lo que estamos haciendo con expresiones como rand () % 6! Una forma de
paliar este problema es usar una expresión diferente:

int dado(void)
{
return (int) ((double) rand () / ((double) RAND_MAX + 1) * 6) + 1;
}

La constante RAND_MAX es el mayor número aleatorio que puede devolver rand . La división
hace que el número generado esté en el intervalo [0, 1[.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 163 El programa dado.c siempre genera la misma secuencia de números aleatorios. Para
evitarlo, debes proporcionar una semilla diferente con cada ejecución del programa. El valor de
la semilla se suministra como úncio argumento de la función srand . Modifı́ca dado.c para que
solicite al usuario la introducción del valor semilla.
.............................................................................................
Un uso tı́pico de las funciones sin parámetros es la lectura de datos por teclado que deben
satisfacer una serie de restricciones. Esta función, por ejemplo, lee un número 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úmero 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ón de menús de usuario con lectura de la opción seleccionada
por el usuario:

Introducción a la Programación con C 145


3.4 Procedimientos 2004/02/10-16:33

1 int menu_principal (void)


2 {
3 int opcion;
4

5 do {
6 printf ("1) Alta usuario\n");
7 printf ("2) Baja usuario\n");
8 printf ("3) Consulta usuario\n");
9 printf ("4) Salir\n");
10

11 printf ("Opción: "); scanf ("%d", &opcion);


12 if (opcion < 1 || opcion > 4)
13 printf ("Opción no válida.\n");
14 } while (opcion < 1 || opcion > 4);
15

16 return opcion;
17 }

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 164 Diseña una función que lea por teclado un entero positivo y devuelva el valor leı́do.
Si el usuario introduce un número negativo, la función advertirá del error por pantalla y leerá
nuevamente el número cuantas veces sea menester.
.............................................................................................

3.4. Procedimientos
Un procedimiento, como recordarás, es una función 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ámetros.
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ón del procedimien-
to.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 165 Diseña un procedimiento que reciba un entero n y muestre por pantalla n asteriscos
seguidos con un salto de lı́nea al final.
· 166 Diseña un procedimiento que, dado un valor de n, dibuje con asteriscos un triángulo
rectángulo cuyos catetos midan n caracteres. Si n es 5, por ejemplo, el procedimiento mostrará
por pantalla este texto:
1 *
2 **
3 ***
4 ****
5 *****

Puedes usar, si te conviene, el procedimiento desarrollado en el ejercicio anterior.


· 167 Diseña un procedimiento que reciba un número entero entre 0 y 99 y muestre por
pantalla su trascripción escrita. Si le suministramos, por ejemplo, el valor 31, mostrará el texto
((treinta y uno)).
.............................................................................................

146 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

3.5. Paso de parámetros


3.5.1. Parámetros escalares: paso por valor
Aunque modifiques el valor de un parámetro escalar en una función o procedimiento, el valor
de la variable pasada como argumento permanece inalterado. La función bits del siguiente
programa, por ejemplo, modifica en su cuerpo el valor de su parámetro 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úmero 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ón se modifica el valor del parámetro num (que toma el valor de numero
en la llamada). Un parámetro es como una variable local, sólo 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ámetro
por valor.
Llegados a este punto conviene que nos detengamos a estudiar cómo se gestiona la memoria
en las llamadas a función.

3.5.2. Organización de la memoria: la pila de llamadas a función


En C las variables locales se gestionan, al igual que en Python, mediante una pila. Cada función
activada se representa en la pila mediante un registro de activación o trama de activación. Se
trata de una zona de memoria en la que se almacenan las variables locales y parámetros junto
a otra información, como el punto desde el que se llamó a la función.
Cuando iniciamos la ejecución del programa numbits.c, se activa automáticamente la fun-
ción main. En élla tenemos dos variables locales: numero y bitsnumero.

bitsnumero
main
numero

Introducción a la Programación con C 147


3.5 Paso de parámetros 2004/02/10-16:33

Si el usuario teclea el valor 128, éste se almacena en numero:

bitsnumero
main
numero 128

Cuando se produce la llamada a la función bits, se crea una nueva trama de activación:

b
bits
num
llamada desde lı́nea 21

bitsnumero
main
numero 128

El parámetro num recibe una copia del contenido de numero y se inicializa la variable local b
con el valor 0:

b 0
bits
num 128
llamada desde lı́nea 21

bitsnumero
main
numero 128

Tras ejecutar el bucle de bits, la variable b vale 8. Observa que aunque num ha modificado su
valor y éste provenı́a originalmente de numero, el valor de numero no se altera:

b 8
bits
num 0
llamada desde lı́nea 21

bitsnumero
main
numero 128

La trama de activación de bits desaparece ahora, pero dejando constancia del valor devuelto
por la función:

return 8

bitsnumero
main
numero 128

Y, finalmente, el valor devuelto se copia en bitsnumero:

bitsnumero 8
main
numero 128

148 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

Como ves, las variables locales sólo ((viven)) durante la ejecución de cada función. C obtiene
una copia del valor de cada parámetro y la deja en la pila. Cuando modificamos el valor de un
parámetro en el cuerpo de la función, 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ón. Representaremos dicha zona como un área enmarcada con una lı́nea discontı́nua.
Cuando se inicia la ejecución del programa, ésta es la situación:
variables globales

main bitsnumero numero

En main se da valor a la variable global numero:


variables globales

main bitsnumero numero 128

Y se llama a continuación a bits sin argumento alguno:

variables globales
bits
b
llamada desde lı́nea 22

main bitsnumero numero 128

Introducción a la Programación con C 149


3.5 Paso de parámetros 2004/02/10-16:33

El cálculo de bits modifica el valor de numero. Tras la primera iteración del bucle while, ésta
es la situación:

variables globales
bits
b 1
llamada desde lı́nea 22

main bitsnumero numero 64

Cuando finaliza la ejecución de bits tenemos:


variables globales

return 8

main bitsnumero numero 0

Entonces se copia el valor devuelto en bitsnumero:


variables globales

main bitsnumero 8 numero 0

El mensaje que obtenemos en pantalla es:



Introduce un entero positivo: 128
Hay 8 bits, pero ahora ’numero’ vale 0.

Bueno. Ahora sabes qué pasa con las variables globales y cómo acceder a ellas desde las
funciones. Pero repetimos lo que te dijimos al aprender Python: pocas veces está justificado
acceder a variables globales, especialmente cuando estás aprendiendo. Evı́talas.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 168 Estudia este programa y muestra gráficamente 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;

150 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

23 j = 20;
24 printf ("%d\n", sumatorio(i, j));
25 return 0;
26 }

· 169 Este programa muestra por pantalla los 10 primeros números primos. La función si-
guiente genera cada vez un número primo distinto. Gracias a la variable global ultimoprimo la
función ((recuerda)) cuál fue el último número 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ón siguienteprimo y
cuando ésta 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úmeros primos\n");


26 for (i=0; i<10; i++)
27 printf ("%d\n", siguienteprimo() );
28 return 0;
29 }
.............................................................................................
No hay problema con que las variables locales a una función sean vectores. Su contenido
se almacena siempre en la pila. Este programa, por ejemplo, cuenta la cantidad de números
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óstenes. El vector con el que se efectúa la criba es una variable local
a la función que efectúa 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úmero de primos entre 1 y n.


6 {
7 char criba[N] ;
8 int i, j, numprimos;
9

10 /* Comprobemos que el argumento es válido */


11 if (n >= N)
12 return -1; // Devolvemos −1 si no es válido.
13

14 /* Inicialización */

Introducción a la Programación con C 151


3.5 Paso de parámetros 2004/02/10-16:33

15 criba[0] = 0;
16 for (i=1; i<n; i++)
17 criba[i] = 1;
18

19 /* Criba de Eratóstenes */
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álculo. El mayor valor permitido es %d.\n", N-1);
43 else
44 printf ("Primos entre 1 y %d: %d\n", hasta, cantidad );
45 return 0;
46 }

Cuando el programa inicia su ejecución, se crea una trama de activación 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:

hasta 6
main
cantidad

Se efectúa entonces (lı́nea 40) la llamada a cuenta_primos, con lo que se crea una nueva tra-
ma de activación. En ella se reserva memoria para todas las variables locales de cuenta_primos:

n
0 1 2 3 4 5 6 7 8 9

criba
cuenta primos
j
i
numprimos
llamada desde lı́nea 40

hasta 6
main
cantidad

152 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

Observa que el vector criba ocupa memoria en la propia trama de activación. Completa tú
mismo el resto de acciones ejecutadas por el programa ayudándote de una traza de la pila de
llamadas a función con gráficos 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ón. Es hora de matizar esa afirmación. Los vectores locales a una función pueden
determinar su talla en tiempo de ejecución. Veamos un ejemplo:

eratostenes 2.c eratostenes.c


1 #include <stdio.h>
2

3 int cuenta_primos(int n) //Cuenta el número de primos entre 1 y n.


4 {
5 char criba[n] ;
6 int i, j, numprimos;
7

8 /* Inicialización */
9 criba[0] = 0;
10 for (i=1; i<n; i++)
11 criba[i] = 1;
12

13 /* Criba de Eratóstenes */
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: %d\n", hasta, cantidad );
36 return 0;
37 }

Fı́jate en cómo hemos definido el vector criba: la talla no es un valor constante, sino n, un
parámetro cuyo valor es desconocido hasta el momento en que se ejecute la función. Esta es
una caracterı́stica de C99 y supone una mejora interesante del lenguaje.

3.5.4. Parámetros vectoriales: paso por referencia


Este programa ilustra el modo en que podemos declarar y pasar parámetros vectoriales a una
función:
pasa vector.c pasa vector.c
1 #include <stdio.h>
2

3 #define TALLA 3

Introducción a la Programación con C 153


3.5 Paso de parámetros 2004/02/10-16:33

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: %d\n", i, v[i]);
22 }
23 incrementa(v) ;
24 printf ("Después de llamar a incrementa:\n");
25 for (i=0; i<TALLA; i++)
26 printf ("%d: %d\n", i, v[i]);
27 return 0;
28 }
Fı́jate en cómo se indica que el parámetro a es un vector de enteros: añadiendo un par de
corchetes a su identificador. En la lı́nea 23 pasamos a incrementa el vector v. ¿Qué ocurre
cuando modificamos componentes del parámetro 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és 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ámetro en las llamadas
a función.
Cuando se pasa un parámetro vectorial a una función no se efectúa una copia de su contenido
en la pila: sólo se copia la referencia a la posición de memoria en la que empieza el vector.
¿Por qué? Por eficiencia: no es infrecuente que los programas manejen vectores de tamaño
considerable; copiarlos cada vez en la pila supondrı́a invertir una cantidad de tiempo que, para
vectores de tamaño medio o grande, podrı́a ralentizar drásticamente la ejecución del programa.
La aproximación adoptada por C hace que sólo sea necesario copiar en la pila 4 bytes, que es
lo que ocupa una dirección de memoria. Y no importa cuán grande o pequeño sea un vector: la
dirección de su primer valor siempre ocupa 4 bytes.
Veamos gráficamente, pues, qué ocurre en diferentes instantes de la ejecución del programa.
Justo antes de ejecutar la lı́nea 23 tenemos esta disposición de elementos en memoria:

0 1 2

v 0 1 2
main

i 3

En el momento de ejecutar la lı́nea 10 por primera vez, en la función incrementa, la memoria


presenta este aspecto:

154 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

a
incrementa
i 0
llamada desde lı́nea 23

0 1 2

v 0 1 2
main

i 3

¿Ves? El parámetro 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ón:

a
incrementa
i 3
llamada desde lı́nea 23

0 1 2

v 1 2 3
main

i 3

Y una vez ha finalizado la ejecución de incrementa, ésta otra:

0 1 2

v 1 2 3
main

i 3

¿Y qué ocurre cuando el vector es una variable global? Pues básicamente lo mismo: las refe-
rencias no tienen por qué ser direcciones de memoria de la pila. Este programa es básicamente
idéntico al anterior, sólo 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ón a la Programación con C 155


3.5 Paso de parámetros 2004/02/10-16:33

21 v[i] = i;
22 printf ("%d: %d\n", i, v[i]);
23 }
24 incrementa(v) ;
25 printf ("Después de llamar a incrementa:\n");
26 for (i=0; i<TALLA; i++)
27 printf ("%d: %d\n", i, v[i]);
28 return 0;
29 }

Analicemos qué ocurre en diferentes instantes de la ejecución del programa. Justo antes de
ejecutar la lı́nea 24, existen las variables locales a main y las variables globales:
variables globales

0 1 2

main i 3 v 0 1 2

Al llamar a incrementa se suministra un puntero a la zona de memoria de variables globales,


pero no hay problema alguno: el parámetro a es un puntero que apunta a esa dirección.

a
variables globales

incrementa i 0
llamada desde lı́nea 24
0 1 2

main i 3 v 0 1 2

Los cambios al contenido de a se manifiestan en v:

a
variables globales

incrementa i 3
llamada desde lı́nea 24
0 1 2

main i 3 v 1 2 3

Y una vez ha finalizado la ejecución de incrementa, el contenido de v queda modificado:


variables globales

0 1 2

main i 3 v 1 2 3

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 170 Diseña un programa C que manipule polinomios de grado menor o igual que 10. Un
polinomio se representará con un vector de float de tamaño 11. Si p es un vector que representa
un polinomio, p[i] es el coeficiente del término de grado i. Diseña un procedimiento suma con
el siguiente perfil:
void suma(float p[], float q[], float r[])
El procedimiento modificará r para que contenga el resultado de sumar los polinomios p y q.
· 171 Diseña una función que, dada una cadena y un carácter, diga cuántas veces aparece el
carácter en la cadena.

156 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

.............................................................................................
Hemos visto cómo pasar vectores a funciones. Has de ser consciente de que no hay forma de
saber cuántos elementos tiene el vector dentro de una función: fı́jate en que no se indica cuántos
elementos tiene un parámetro vectorial. Si deseas utilizar el valor de la talla de un vector tienes
dos posibilidades:
1. saberlo de antemano,
2. o proporcionarlo como parámetro 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ón, la función inicializa sólo 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ás un error de acceso a memoria que no te está reservada, pues el bucle recorre TALLA1
componentes, aunque y sólo tenga TALLA2. Ese error puede abortar la ejecución del programa
o, peor aún, no haciéndolo pero alterando la memoria de algún 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ón, pero ha mostrado 20 valores del vector y, que


sólo tiene 10.
¿Cómo podemos diseñar una función que pueda trabajar tanto con el vector x como con
el vector y? Siguiendo la segunda aproximación propuesta, es decir, pasando como parámetro
adicional la talla del vector en cuestión:

Introducción a la Programación con C 157


3.5 Paso de parámetros 2004/02/10-16:33

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ón inicializa con inicializa(x, TALLA1) o inicializa(y, TALLA2).
Lo mismo ocurre con imprime. El parámetro talla toma el valor apropiado en cada caso porque
tú se lo estás pasando explı́citamente.
Éste 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ña un procedimiento ordena que ordene un vector de enteros. El procedimiento
recibirá como parámetros un vector de enteros y un entero que indique el tamaño del vector.
· 173 Diseña una función que devuelva el máximo de un vector de enteros. El tamaño del
vector se suministrará como parámetro adicional.
· 174 Diseña una función que diga si un vector de enteros es o no es palı́ndromo (devolviendo
1 o 0, respectivamente). El tamaño del vector se suministrará como parámetro adicional.
· 175 Diseña una función que reciba dos vectores de enteros de idéntica talla y diga si son
iguales o no. El tamaño de los dos vectores se suministrará como parámetro adicional.
· 176 Diseña un procedimiento que reciba un vector de enteros y muestre todos sus com-
ponentes en pantalla. Cada componente se representará separado del siguiente con una coma.
El último elemento irá seguido de un salto de lı́nea. La talla del vector se indicará con un
parámetro adicional.

158 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

· 177 Diseña un procedimiento que reciba un vector de float y muestre todos sus componentes
en pantalla. Cada componente se representará separado del siguiente con una coma. Cada 6
componentes aparecerá un salto de lı́nea. La talla del vector se indicará con un parámetro
adicional.
.............................................................................................

3.5.5. Parámetros escalares: paso por referencia mediante punteros


C permite modificar el valor de variables escalares en una función 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 %d\n", b);
14 incrementa( &b );
15 printf ("Y al final vale %d\n", 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 único parámetro 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ón el valor de un entero, sino el valor de la
dirección de memoria en la que se encuentra un entero.
Fı́jate ahora en cómo pasamos el argumento en la llamada a incrementa de la lı́nea 14, que
es de la forma incrementa(&b). Estamos pasando la dirección 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ón
espera la dirección de memoria de un entero.
Al principio de la ejecución de incrementa tendremos esta situación:

incrementa a
llamada desde lı́nea 14

main b 1

El parámetro 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ón)). Ese asterisco es un operador unario


que hace justo lo contrario que el operador &: dada una dirección de memoria, accede al valor
de la variable apuntada. (Recuerda que el operador & obtenı́a la dirección de memoria de una

Introducción a la Programación con C 159


3.5 Paso de parámetros 2004/02/10-16:33

El & de los parámetros de scanf


Ahora ya puedes entender bien por qué 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é dirección 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ón 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é pasarı́a si en lugar de *a += 1 hubiésemos escrito a += 1? Se hubiera incrementado la
dirección de memoria a la que apunta el puntero, nada más.
¿Y si hubiésemos escrito a++? Lo mismo: hubiésemos incrementado el valor de la dirección
almacenada en a. ¿Y *a++?, ¿funcionarı́a? A primera vista dirı́amos que sı́, pero no funciona
como esperamos. El operador ++ tiene mayor nivel de precedencia que el operador unario *, ası́
que *a++ (post)incrementa la dirección a y accede a su contenido, por ese órden. Nuevamente
habrı́amos incrementado el valor de la dirección de memoria, y no su contenido. Si quieres usar
operadores de incremento/decremento, tendrás que utilizar paréntesis para que los operadores
se apliquen en el orden deseado: (*a)++.
Naturalmente, no sólo puedes acceder ası́ a variables locales, también 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 %d\n", b);
14 incrementa( &b );
15 printf ("Y al final vale %d\n", b);
16 return 0;
17 }

El aspecto de la memoria cuando empieza a ejecutarse la función incrementa es éste:

variables globales

incrementa a
llamada desde lı́nea 14

main b 1

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 178 Diseña un procedimiento que modifique el valor del parámetro de tipo float para
que valga la inversa de su valor cuando éste sea distinto de cero. Si el número es cero, el
procedimiento dejará intacto el valor del parámetro.
Si a vale 2.0, por ejemplo, inversa(&a) hará que a valga 0.5.
· 179 Diseña un procedimiento que intercambie el valor de dos números enteros.
Si a y b valen 1 y 2, respectivamente, la llamada intercambia(&a, &b) hará que a pase a
valer 2 y b pase a valer 1.
· 180 Diseña un procedimiento que intercambie el valor de dos números float.

160 Introducción a la Programación con C



c 2003 Andrés 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ón estamos pasando, realmente, una dirección de
memoria: aquella en la que empieza la zona de memoria reservada para el vector. Cuando
pasamos una variable escalar por referencia, también estamos pasando una dirección de
memoria: aquella en la que empieza la zona de memoria reservada para el valor escalar.
¿Qué diferencia hay entre una y otra dirección? 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 %6d\n", *a , b[0] );
!
8 printf ("%22d %6d\n", 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ón:


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é 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ón de un vector de ints como la dirección de una variable escalar de tipo int.
La conclusión es clara: ((int * a)) e ((int b[])) son sinónimos cuando se declara un
parámetro, 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ónimas y, por tanto, intercambiables, interesa que las uses
((correctamente)), pues ası́ mejorará la legibilidad de tus programas: usa int * cuando quieras
pasar la dirección de un entero y int [] cuando quieras pasar la dirección de un vector de
enteros.

· 181 Diseña un procedimiento que asigne a todos los elementos de un vector de enteros un
valor determinado. El procedimiento recibirá tres datos: el vector, su número de elementos y el
valor que que asignamos a todos los elementos del vector.
· 182 Diseña un procedimiento que intercambie el contenido completo de dos vectores de
enteros de igual talla. La talla se debe suministrar como parámetro.

Introducción a la Programación con C 161


3.5 Paso de parámetros 2004/02/10-16:33

En C sólo hay paso por valor


Este apartado intenta que aprendas a distinguir el paso de parámetros por valor y por
referencia. ¡Pero la realidad es que C sólo tiene paso de parámetros por valor! Cuando pasas
una referencia, estás pasando explı́citamente una dirección de memoria gracias al operador
&, y lo que hace C es copiar dicha dirección en la pila, es decir, pasa por valor una dirección
para simular el paso de parámetros por referencia. La extraña forma de pasar el parámetro
hace que tengas que usar el operador * cada vez que deseas acceder a él en el cuerpo de la
función.
En otros lenguajes, como Pascal, es posible indicar que un parámetro 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ámetro en el cuerpo de la función. Por ejemplo, este
programa Pascal incluye un procedimiento que modifica el valor de su parámetro:

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ón de C que permite el paso de parámetros por referencia. Usa para ello
el carácter & en la declaración del parámetro:

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 %d\n", b);
14 incrementa( b );
15 printf ("Y al final vale %d\n", 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ómo el programa C++ presenta un aspecto muy semejante a uno equivalente
escrito en C.)

· 183 Diseña un procedimiento que asigne a un entero la suma de los elementos de un vector
de enteros. Tanto el entero (su dirección) como el vector se suministrarán como parámetros.
.............................................................................................

Un uso habitual del paso de parámetros por referencia es la devolución de más de un valor
como resultado de la ejecución de una función. Veámoslo con un ejemplo. Diseñemos una función
que, dados un ángulo α (en radianes) y un radio r, calcule el valor de x = r cos(α) e y = r sin(α):

162 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

r y
α
x

No podemos diseñar una función que devuelva los dos valores. Hemos de diseñar un procedi-
miento que devuelva los valores resultantes como parámetros 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ómo 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 ángulo (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ña una función que calcule la inversa de calcula_xy, es decir, que obtenga el valor
del radio y del ángulo a partir de x e y.
· 185 Diseña una función que reciba dos números enteros a y b y devuelva, simultáneamente,
el menor y el mayor de ambos. La función tendrá esta cabecera:
1 void minimax (int a, int b, int * min, int * max )

· 186 Diseña una función que reciba un vector de enteros, su talla y un valor de tipo entero al
que denominamos buscado. La función devolverá (mediante return) el valor 1 si buscado tiene el
mismo valor que algún elemento del vector y 0 en caso contrario. La función devolverá, además,
la distancia entre buscado y el elemento más próximo a él.
La cabecera de la función ha de ser similar a ésta:
1 int busca(int vector [], int talla, int buscado, int * distancia)

Te ponemos un par de ejemplos para que veas qué debe hacer la función.
1 #include <stdio.h>
2

3 #define TALLA 6

Introducción a la Programación con C 163


3.5 Paso de parámetros 2004/02/10-16:33

5 // Define aquı́ la función


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é valor busco?: ");
18 scanf ("%d", &buscado);
19

20 encontrado = busca(v, TALLA, buscado, &distancia);


21 if (encontrado)
22 printf ("Encontré el valor %d.\n", buscado);
23 else
24 printf ("No está. El elemento más próximo está a distancia %d.\n", distancia);
25
?
26 printf (" Qué valor busco ahora?: ");
27 scanf ("%d", &buscado);
28

29 encontrado = busca(v, TALLA, buscado, &distancia);


30 if (encontrado)
31 printf ("Encontré el valor %d.\n", buscado);
32 else
33 printf ("No está. El elemento más próximo está 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é valor busco?: 5
Encontré el valor 5.
? 
Qué valor busco ahora?: 17
No está. El elemento más próximo está a distancia 2.

· 187 Modifica la función del ejercicio anterior para que, además de la distancia al elemento
más próximo, devuelva el valor del elemento más próximo.

· 188 Modifica la función del ejercicio anterior para que, además de la distancia al elemento
más próximo y el elemento más próximo, devuelva el valor de su ı́ndice.
.............................................................................................

3.5.6. Paso de registros a funciones


No sólo puedes pasar escalares y vectores como argumentos, también puedes pasar registros. El
paso de registros es por valor, o sea, copiando el contenido en la pila, a menos que tú mismo
pases un puntero a su dirección de memoria.
Este programa, por ejemplo, define un tipo de datos para representar puntos en un espacio
de tres dimensiones y una función que calcula la distancia de un punto al origen:

164 Introducción a la Programación con C



c 2003 Andrés 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: %f\n", distancia(pto) );


22 return 0;
23 }

Al pasar un registro a la función, 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ón pasando registros por valor puede resultar
ineficiente. Por cierto, no es tan extraño que un registro ocupe cientos de bytes: uno o más de
sus campos podrı́a ser un vector. También en ese caso se estarı́a copiando su contenido ı́ntegro
en la pila.
Eso sı́, como estás pasando una copia, las modificaciones del valor de un campo en el cuerpo
de la función no tendrán efectos perceptibles fuera de la función.
Como te hemos anticipado, también puedes pasar registros por referencia. En tal caso sólo
se estará copiando en la pila la dirección de memoria en la que empieza el registro (y eso son
4 bytes), mida lo que mida éste. Se trata, pues, de un paso de parámetros más eficiente. Eso
sı́, has de tener en cuenta que los cambios que efectúes a cualquier campo del parámetro se
reflejarán en el campo correspondiente de la variable que suministraste como argumento.
Esta función, por ejemplo, define dos parámetros: uno que se pasa por referencia y otro
que se pasa por valor. La función 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ómo hemos accedido a los campos de p. Ahora p es una dirección 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ón de memoria
apuntada por p, y luego al campo x del registro *p, de ahı́ que usemos paréntesis.
Es tan frecuente la notación (*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ón a la Programación con C 165


3.5 Paso de parámetros 2004/02/10-16:33

Recuerda, pues, que dentro de una función se accede a los campos de forma distinta según
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ón (*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án variables de tipo struct Punto, que definimos ası́:
1 struct Punto {
2 float x, y;
3 };

Diseña 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á en pantalla ası́: (2.000000, 0.200000). El procedimiento
muestra_punto recibirá un punto por valor.
Diseña a continuación un procedimiento que permita leer por teclado un punto. El procedi-
miento recibirá por referencia el punto en el que se almacenarán los valores leı́dos.
· 190 La operación de traslación 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ón que reciba dos parámetros de tipo punto y modifique el primero de modo que se
traslade lo que indique el vector.
· 191 La operación de escalado transforma un punto (x, y) en otro (ax, ay), donde a es un
factor de escala (real). Implementa una función que escale un punto de acuerdo con el factor
de escala a que se suministre como parámetro (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ón que rote un punto la cantidad de grados que se especifique.
· 193 La rotación de un punto (x, y) una cantidad de θ radianes alrededor de un punto (a, b)
se puede efectuar con una traslación con el vector (−a, −b), una rotación de θ radianes con
respecto al origen y una nueva traslación con el vector (a, b). Diseña una función que permita
trasladar un punto un número dado de grados alrededor de otro punto.
· 194 Diseña una función 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ña e implementa los siguientes procedimientos para su manipulación:

leer un complejo de teclado;

mostrar un complejo por pantalla;

166 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones


el módulo 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ón 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ño. 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ño 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ón del paso de vectores unidi-
mensionales. Veamos. Aquı́ tienes un programa incorrecto en el que se define una función que
recibe una matriz y devuelve su elemento máximo:

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ón a la Programación con C 167


3.5 Paso de parámetros 2004/02/10-16:33

24 for (j=0; j<TALLA; j++)


25 matriz [i][j] = (i*j) % TALLA;
26

27 printf ("El máximo es %d\n", maximo(matriz ));


28 return 0;
29 }

El compilador no acepta ese programa. ¿Por qué? Fı́jate en la declaración del parámetro. ¿Qué
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ón se obtiene sumando a la dirección a el valor i * COLUMNAS +
j)), donde COLUMNAS es el número de columnas de la matriz a (en nuestro caso, serı́a TALLA).
Pero, ¿cómo sabe la función cuántas columnas tiene a? ¡No hay forma de saberlo viendo una
definición del estilo int a[][]!
La versión correcta del programa debe indicar explı́citamente cuántas 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áximo es %d\n", maximo(matriz ));


28 return 0;
29 }
No ha sido necesario indicar cuántas filas tiene la matriz (aunque somos libres de hacerlo). La
razón es sencilla: el número de filas no hace falta para calcular la dirección en la que reside el
valor a[i][j].
Ası́ pues, en general, es necesario indicar explı́citamente el tamaño de cada una de las
dimensiones del vector, excepto el de la primera (que puedes declarar o no, a voluntad). Sólo
ası́ obtiene C información suficiente para calcular los accesos a elementos del vector en el cuerpo
de la función.
Una consecuencia de esta restricción es que no podremos definir funciones capaces de tra-
bajar con matrices de tamaño arbitrario. Siempre hemos de definir explı́citamente el tamaño
de cada dimensión excepto de la primera. Habrá una forma de superar este inconveniente, pero
tendremos que esperar al siguiente capı́tulo para poder estudiarla.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 197 Vamos a diseñar un programa capaz de jugar al tres en raya. El tablero se representará
con una matriz de 3 × 3. Las casillas serán caracteres. El espacio en blanco representará una
casilla vacı́a; el carácter ’o’ representará una casilla ocupada con un cı́rculo y el carácter ’x’
representará una casilla marcada con una cruz.

168 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

Diseña una función que muestre por pantalla el tablero.

Diseña una función que detecte si el tablero está lleno.

Diseña una función que detecte si algún jugador consiguió hacer tres en raya.

Diseña una función que solicite al usuario la jugada de los cı́rculos y modifique el tablero
adecuadamente. La función debe detectar si la jugada es válida o no.

Diseña una función que, dado un tablero, realice la jugada que corresponde a las cruces.
En una primera versión, haz que el ordenador ponga la cruz en la primera casilla libre.
Después, modifica la función para que el ordenador realice la jugada más inteligente.

Cuando hayas diseñado 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á rodeada por 0 o 1 celdas vivas, muere de soledad.

Si una celda viva está rodeada por 4 celdas vivas, muere por superpoblación.

Si una celda viva está rodeada por 2 o 3 celdas vivas, sigue viva.

Una celda muerta sólo resucita si está rodeada por 3 celdas vivas.

Diseña una función 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ón modificará la matriz de acuerdo
con las reglas del juego de la vida. (Avisos: Necesitarás una matriz auxiliar. Las celdas de los
bordes no tienen 8 vecinos, sino 3 o 5.)
A continuación, monta un programa que permita al usuario introducir una disposición 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ón inicial curiosa:
Configuración inicial:
__________
______xxx_
__________
__________
___xxx____
__xxx_____
__________
__________
__________
__________
Ciclos: 3
_______x__
_______x__
_______x__
____x_____
__x__x____
__x__x____
___x______
__________
__________
__________

__________
______xxx_
__________
__________
___xxx____
__xxx_____

Introducción a la Programación con C 169


3.5 Paso de parámetros 2004/02/10-16:33

__________
__________
__________
__________

_______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á libre, el usuario es informado de cuántas 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ún. Las casillas con un número han sido
descubiertas y sus casillas vecinas contienen tantas bombas como se indica en el número. 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 él al jugador.
Los niveles de dificultad son:

fácil: 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ñar funciones para desempeñar cada una de las acciones básicas 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úmero de bombas vecinas,

dado un tablero y las coordenadas de una casilla, modificar el tablero para indicar que la
casilla en cuestión ya ha sido descubierta,

dado un tablero, mostrar su contenido en pantalla,

etc.
.............................................................................................

170 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

3.5.8. Tipos de retorno válidos


Una función puede devolver valores de cualquier tipo escalar o de registros, pero no puede
devolver vectores3 . La razón es simple: la asignación funciona con valores escalares y registros,
pero no con vectores.
Ya hemos visto cómo devolver valores escalares. A tı́tulo ilustrativo te presentamos un
ejemplo de definición de registro y definición de función que recibe como parámetros un punto
(x, y) y un número 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ón de si es posible devolver vectores


cuando estudiemos la gestión de memoria dinámica.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 200 Vuelve a implementar las funciones de manipulación de puntos en el plano (ejerci-
cios 189–194) para que no modifiquen el valor del registro struct Punto que se suministra como
parámetro. En su lugar, devolverán el punto resultante como valor de retorno de la llamada a
función.
· 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ámetros.
.............................................................................................

3.5.9. Un ejercicio práctico: miniGalaxis


Pongamos en práctica lo aprendido diseñando una versión 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áufragos
espaciales y nuestro objetivo es descubrir dónde se encuentran. Contamos para ello con una
sonda espacial que podemos activar en cualquier casilla del tablero. La sonda dispara una
señal en las cuatro direcciones cardinales que es devuelta por unos dispositivos que llevan los
náufragos. La sonda nos dice cuántos náufragos espaciales han respondido, pero no desde qué
direcciones enviaron su señal de respuesta. Cuando activamos la sonda en las coordenadas
exactas en las que se encuentra un naúfrago, lo damos por rescatado. Sólo disponemos de 20
sondas para efectuar el rescate, ası́ que las hemos de emplear juiciosamente. De lo contrario, la
muerte de inocentes pesará sobre nuestra conciencia.
Lo mejor será que te hagas una idea precisa del juego jugando. Al arrancar aparece esta
información en pantalla:
ABCDEFGHIJKLMNOPQRST
0 ++++++++++++++++++++
1 ++++++++++++++++++++
2 ++++++++++++++++++++
3 ++++++++++++++++++++
4 ++++++++++++++++++++
3 Al
menos no hasta que sepamos más de la gestión de memoria dinámica
4 El
nombre y la descripción 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ás tarde, Eric Raymond lo reescribió para que fuera ejecutable en Unix.

Introducción a la Programación con C 171


3.5 Paso de parámetros 2004/02/10-16:33

5 ++++++++++++++++++++
6 ++++++++++++++++++++
7 ++++++++++++++++++++
8 ++++++++++++++++++++
Hay 5 náufragos.
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úmeros para las filas. El ordenador nos informa de que aún quedan
5 náufragos por rescatar y que disponemos de 20 sondas. Se ha detenido mostrando el mensaje
((Coordenadas:)): está esperando a que digamos en qué 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úscula o mayúscula. 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áufragos.
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úmero de naúfragos 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áufrago. 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áufragos.
Dispones de 18 sondas.
Coordenadas:

En la casilla de coordenadas 3I aparece un uno: la sonda ha detectado la presencia de un


náufrago en alguna de las 4 direcciones. Sigamos. Probemos en 0I:

Coordenadas: i0
ABCDEFGHIJKLMNOPQRST
0 ........2...........
1 +.++++++.+++++++++++
2 +.++++++.+++++++++++
3 ........1...........
4 +.++++++.+++++++++++
5 .0..................
6 +.++++++.+++++++++++

172 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

7 +.++++++.+++++++++++
8 +.++++++.+++++++++++
Hay 5 náufragos.
Dispones de 17 sondas.
Coordenadas:

Dos náufragos detectados. Parece probable que uno de ellos esté 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áufragos.
Dispones de 16 sondas.
Coordenadas:

¡Bravo! Hemos encontrado a uno de los náufragos. En el tablero se muestra con una X. Ya
sólo quedan 4.
Bueno. Con esta partida inacabada puedes hacerte una idea detallada del juego. Diseñemos
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á bien disponer de constantes que almacenen
el número de filas y columnas para usarlas en la definición 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ñar
una función especial para ello. ¿Por qué? Para mantener main razonablemente pequeño 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ón a la Programación con C 173


3.5 Paso de parámetros 2004/02/10-16:33

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úmero de columnas de la misma.5 En el interior de la función


se modifica el contenido de la matriz. Los cambios afectarán 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ás de una ocasión. Podemos
diseñar 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ás, del contenido del tablero, el nombre de


las columnas y el número 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úmeros enteros).
Cuando pidamos unas coordenadas al usuario lo haremos con una sentencia como ésta:
5 No hemos usado el nombre espacio, sino tablero, con el único objetivo de resaltar que el parámetro puede ser

cualquier matriz (siempre que su dimensión se ajuste a lo esperado), aunque nosotros sólo usaremos la matriz
espacio como argumento. Si hubiésemos usado el mismo nombre, es probable que hubiésemos alimentado la
confusión entre parámetros y argumentos que experimentáis algunos.

174 Introducción a la Programación con C



c 2003 Andrés 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án en una cadena. Nos convendrá disponer, pues, de una
función que ((traduzca)) esa cadena a un par de números 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éricamente 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úscula o mayúscula) en
14 * cualquier orden a una fila y columna descritas numéricamente.
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ón (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ón
es algo más complicada. Una razón para ello es que efectúa cierto tratamiento de errores. ¿Por
qué? 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ón mediante parámetros pasados por referencia,

y la función devuelve un valor que indica si se detectó o no un error (devuelve 0 si hubo


error, y 1 en caso contrario).
De este modo es posible invocar a la función 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álidas. Inténtelo de nuevo.\nCoordenadas: ");

Introducción a la Programación con C 175


3.5 Paso de parámetros 2004/02/10-16:33

5 scanf ("%s", coordenadas);


6 }
7 ...

Sigamos. Hemos de disponer ahora 5 náufragos 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ón y reduciendo notablemente el
interés del juego ;-). ¿Qué hacer? Una posibilidad consiste en usar una matriz adicional en la
que poder disponer los náufragos. 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áufrago en alguna posición
determinada del tablero. Si bien es una posibilidad interesante (y te la propondremos más
adelante como ejercicio), nos decantamos por seguir una diferente que nos permitirá 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ón de un náufrago 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áufragos de talla MAX_NAUFRAGOS. Aunque el juego indica que hemos de
trabajar con 5 náufragos, usaremos un campo adicional con la cantidad de náufragos realmente
almacenados en el vector. De ese modo resultará sencillo modificar el juego (como te propone-
mos en los ejercicios al final de esta sección) para que se juegue con un número de náufragos
seleccionado por el usuario.
Guardaremos los náufragos 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áufrago en una posición aletoria del tablero. Esta función (errónea) se encarga de ello:
...
#include <stdlib.h>
...
void pon_naufragos(struct GrupoNaufragos * grupoNaufragos, int cantidad )
/* Situa aleatoriamente cantidad náufragos en la estructura grupoNaufragos. */
/* PERO LO HACE MAL. */
{
int fila, columna;

grupoNaufragos->cantidad = 0;

176 Introducción a la Programación con C



c 2003 Andrés 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é está mal? Primero hemos de entenderla bien. Analicémosla paso a paso. Empecemos
por la cabecera: la función tiene dos parámetros, uno que es una referencia (un puntero) a un
registro de tipo struct GrupoNaufragos y un entero que nos indica cuántos náufragos hemos
de poner al azar. La rutina empieza inicializando a cero la cantidad de náufragos ya dispuestos
mediante una lı́nea como ésta:
grupoNaufragos -> cantidad = 0;

¿Entiendes por qué se usa el operador flecha?: la variable grupoNaufragos es un puntero, ası́ que
hemos de acceder a la información apuntada antes de acceder al campo cantidad . Podrı́amos
haber escrito esa misma lı́nea ası́:
(* grupoNaufragos ).cantidad = 0;

pero hubiera resultado más incómodo (e ilegible). A continuación, la función repite cantidad
veces la acción consistente en seleccionar una fila y columna al azar (mediante la función rand
de stdlib.h) y lo anota en una posición del vector de náufragos. 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ás claro, ¿no? Piensa que grupoNaufragos->naufrago es un vector como cualquier otro, ası́
que la expresión grupoNaufragos->naufrago[0] accede a su primer elemento. ¿De qué tipo es
ese elemento? De tipo struct Naufrago. Un elemento de ese tipo tiene un campo fila y se
accede a él 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ón te debe resultar fácil de leer ahora. Volvamos a la cuestión principal: ¿por qué
está mal diseñada esa función? Fácil: porque puede ubicar dos náufragos en la misma casilla
del tablero. ¿Cómo corregimos el problema? Asegurándonos de que cada náufrago ocupa una
casilla diferente. Tenemos dos posibilidades:
Generar la posición de cinco náufragos 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ón de cada náufrago de una en una y comprobando cada vez que
ésta es distinta de la de todos los náufragos anteriores. Si no lo es, volvemos a generar la
posición de este náufrago concreto; si lo es, pasamos al siguiente.
La segunda resulta más sencilla de implementar y es, a la vez, más eficiente. Aquı́ la tienes
implementada:
void pon_naufragos(struct GrupoNaufragos * grupoNaufragos, int cantidad )
/* Sitúa aleatoriamente cantidad náufragos 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ón a la Programación con C 177


3.5 Paso de parámetros 2004/02/10-16:33

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á bien disponer de una función que muestre por pantalla la ubicación y estado de
cada náufrago. Esta función no resulta útil 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ón mientras jugamos partidas de prueba y, una vez dado por bueno el programa, no
llamarla más. En cualquier caso, aquı́ la tienes:
void muestra_naufragos(struct GrupoNaufragos grupoNaufragos)
/* Muestra por pantalla las coordenadas de cada náufrago e informa de si sigue perdido.
* Útil para depuración 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áufrago %d en coordenadas %s ", i, coordenadas);
if (grupoNaufragos.naufrago[i].encontrado)
printf ("ya ha sido encontrado.\n");
else
printf ("sigue perdido.\n");
}
}

La función está bien, pero podemos mejorarla. Fı́jate en cómo pasamos su parámetro: por valor.
¿Por qué? Porque no vamos a modificar su valor en el interior de la función. En principio, la
decisión de pasarlo por valor está bien fundamentada. No obstante, piensa en qué ocurre cada
vez que llamamos a la función: como un registro de tipo struct GrupoNaufragos ocupa 64
bytes (haz cuentas y compruébalo), cada llamada a la función obliga a copiar 64 bytes en la
pila. El problema se agravarı́a si en lugar de trabajar con un número máximo de 5 náufragos lo
hiciéramos 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ón
de la función efectúa el paso por referencia:
void muestra_naufragos( struct GrupoNaufragos * grupoNaufragos)
/* Muestra por pantalla las coordenadas de cada náufrago e informa de si sigue perdido.
* Útil para depuración 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áufrago %d en coordenadas %s ", i, coordenadas);
if (grupoNaufragos -> naufrago[i].encontrado)

178 Introducción a la Programación con C



c 2003 Andrés 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áufrago 0 en coordenadas 1G sigue perdido.
Náufrago 1 en coordenadas 0P sigue perdido.
Náufrago 2 en coordenadas 5P sigue perdido.
Náufrago 3 en coordenadas 1M sigue perdido.
Náufrago 4 en coordenadas 6B sigue perdido.

Bien: cada náufrago ocupa una posición diferente. Ejecutémoslo de nuevo



$ minigalaxis
Náufrago 0 en coordenadas 1G sigue perdido.
Náufrago 1 en coordenadas 0P sigue perdido.
Náufrago 2 en coordenadas 5P sigue perdido.
Náufrago 3 en coordenadas 1M sigue perdido.
Náufrago 4 en coordenadas 6B sigue perdido.

¡Eh! ¡Se han ubicado en las mismas posiciones! ¿Qué gracia tiene el juego si en todas las
partidas aparecen los náufragos en las mismas casillas? ¿Cómo es posible que ocurra algo ası́?
¿No se generaba su ubicación al azar? Sı́ y no. La función rand genera números pseudoaleatorios.
Utiliza una fórmula matemática que genera una secuencia de números de forma tal que no
podemos efectuar una predicción del siguiente (a menos que conozcamos la fórmula, claro está).
La secuencia de números se genera a partir de un número inicial: la semilla. En principio, la
semilla es siempre la misma, ası́ que la secuencia de números es, también, siempre la misma.
¿Qué 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ón 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úmero aleatorio para generar
números aleatorios! Mmmmm. Tranquilo, hay una solución: consultar el reloj del ordenador y
usar su valor como semilla. La función time (disponible incluyendo time.h) nos devuelve el
número 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ón:
1 ...
2 #include <time.h>
3 ...
4

Introducción a la Programación con C 179


3.5 Paso de parámetros 2004/02/10-16:33

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áufrago 0 en coordenadas 6K sigue perdido.
Náufrago 1 en coordenadas 5L sigue perdido.
Náufrago 2 en coordenadas 6E sigue perdido.
Náufrago 3 en coordenadas 3I sigue perdido.
Náufrago 4 en coordenadas 8T sigue perdido.

¡Bravo! Son valores diferentes de los anteriores. Ejecutemos nuevamente el programa:



$ minigalaxis
Náufrago 0 en coordenadas 2D sigue perdido.
Náufrago 1 en coordenadas 4H sigue perdido.
Náufrago 2 en coordenadas 5J sigue perdido.
Náufrago 3 en coordenadas 4E sigue perdido.
Náufrago 4 en coordenadas 7G sigue perdido.

¡Perfecto! A otra cosa.


Ya hemos inicializado el tablero y dispuesto los náufragos en posiciones al azar. Diseñemos
una función para el lanzamiento de sondas. La función (que será un procedimiento) recibirá un
par de coordenadas, el tablero de juego y el registro que contiene la posición de los náufragos
y hará lo siguiente:

modificará el tablero de juego sustituyendo los sı́mbolos ’+’ por ’.’ en las direcciones
cardinales desde el punto de lanzamiento de la sonda,

y modificará la casilla en la que se lanzó la sonda indicando el número de náufragos


detectados, o marcándola con una ’X’ si hay un náufrago 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áufrago 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;

180 Introducción a la Programación con C



c 2003 Andrés 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áufrago 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úmero de náufragos detectados.
39 }

Esta función se ayuda con otras dos: hay_naufrago y rescate. La primera nos indica si hay
un náufrago en una casilla determinada:
1 int hay_naufrago(int fila, int columna, const struct GrupoNaufragos * grupoNaufragos)
2 /* Averigua si hay un náufrago 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áufrago 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ón más 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ón a la Programación con C 181


3.5 Paso de parámetros 2004/02/10-16:33

15 printf ("Coordenadas: "); scanf ("%s", coordenadas);


16 while (!de_numero_y_letra_a_fila_y_columna(coordenadas, &fila, &columna)) {
17 printf ("Coordenadas no válidas. Inténtelo 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ándo debe finalizar el bucle while exterior? Bien cuando hayamos rescatado a todos los
náufragos, 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úmero de náufragos 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áufragos\n", perdidos(&losNaufragos));
34 printf ("Dispones de %d sondas\n", 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álidas. Inténtelo 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ón: %d puntos.\n", SONDAS - sondas_disponibles);
47 else
48 printf ("Has perdido. Por tu culpa han muerto %d náufragos\n",

182 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

49 perdidos(&losNaufragos));
50

51 return 0;
52 }

Hemos definido una nueva función, perdidos, que calcula el número de náufragos que per-
manecen perdidos.
Y ya está. 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ón 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éricamente 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úscula o mayúscula) en
35 * cualquier orden a una fila y columna descritas numéricamente.
36 */
38 {
39 printf (">>> %s\n", coordenadas);
40 if (strlen(coordenadas) != 2)
41 return 0;
42 if (coordenadas[0] >= ’0’ && coordenadas[0] <= ’8’ && isalpha(coordenadas[0])) {
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áufragos
57 ****************************************/

Introducción a la Programación con C 183


3.5 Paso de parámetros 2004/02/10-16:33

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áufragos 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áufrago 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áufrago 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)

184 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

123 /* Cuenta el número de náufragos 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 * Útil para depuración 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ón a la Programación con C 185


3.5 Paso de parámetros 2004/02/10-16:33

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áufrago 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áufrago 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úmero de náufragos 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áufragos\n", perdidos(&losNaufragos));
242 printf ("Dispones de %d sondas\n", 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álidas. Inténtelo de nuevo.\nCoordenadas: ");
246 scanf ("%s", coordenadas);
247 }
248 lanzar_sonda(fila, columna, espacio, &losNaufragos);
249 muestra_tablero(espacio);
250 sondas_disponibles--;
251 }
252

186 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

253 if (perdidos(&losNaufragos) == 0)
254 printf ("Has ganado. Puntuación: %d puntos.\n", SONDAS - sondas_disponibles);
255 else
256 printf ("Has perdido. Por tu culpa han muerto %d náufragos\n",
257 perdidos(&losNaufragos));
258

259 return 0;
260 }

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 202 Reescribe el programa para que no se use un variable de tipo struct GrupoNaufragos
como almacén del grupo de náufragos, sino una matriz paralela a la matriz espacio.
Cada náufrago se representará con un ’*’ mientras permanezca perdido, y con un ’X’
cuando haya sido descubierto.
· 203 Siempre que usamos rand en miniGalaxis calculamos un par de números aleatorios.
Hemos definido un nuevo tipo y una función:
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áufragos 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úmeros aleatorios, hemos mo-
dificado el programa de este modo:
1 struct Naufrago naufrago_al_azar (void)
2 {

Introducción a la Programación con C 187


3.6 Recursión 2004/02/10-16:33

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áufragos 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á el número de náufragos perdidos (con un máximo de 20) y el número de sondas
disponibles.
· 206 Hemos construido una versión simplificada de Galaxis. El juego original sólo se dife-
rencia de éste 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ón
Es posible definir funciones recursivas en C. La función factorial de este programa, por ejemplo,
define un cálculo recursivo del factorial:
factorial recursivo.c factorial recursivo.c
1 #include <stdio.h>
2

188 Introducción a la Programación con C



c 2003 Andrés 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úmero entero positivo: ");


16 scanf ("%d", &valor );
17 printf ("El factorial de %d vale: %d\n", valor , factorial (valor ));
18

19 return 0;
20 }

Nada nuevo. Ya conoces el concepto de recursión de Python. En C es lo mismo. Tiene interés,


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á esta configuración:

factorial n 3
llamada desde lı́nea 8

factorial n 4
llamada desde lı́nea 8

factorial n 5
llamada desde lı́nea 17

main valor 5

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 207 Diseña una función que calcule recursivamente xn . La variable x será de tipo float y
n de tipo int.
· 208 Diseña una función recursiva que calcule el n-ésimo número de Fibonacci.
· 209 Diseña una función recursiva para calcular el número combinatorio n sobre m sabiendo
que
 
n
= 1,
n
 
n
= 1,
0
     
n n−1 n−1
= + .
m m m−1
· 210 Diseña un procedimiento recursivo llamado muestra_bin que reciba un número en-
tero positivo y muestre por pantalla su codificación en binario. Por ejemplo, si llamamos a
muestra_bin(5), por pantalla aparecerá el texto ((101)).
.............................................................................................

3.6.1. Un método recursivo de ordenación: mergesort


Vamos a estudiar ahora un método recursivo de ordenación de vectores: mergesort (que se
podrı́a traducir por ordenación por fusión o mezcla). Estudiemos primero la aproximación que

Introducción a la Programación con C 189


3.6 Recursión 2004/02/10-16:33

sigue considerando un procedimiento equivalente para ordenar las 12 cartas de un palo de la


baraja de cartas. La ordenación por fusión de un palo de la baraja consiste en lo siguiente:
Dividir el paquete de cartas en dos grupos de 6 cartas;
ordenar por fusión el primer grupo de 6 cartas;
ordenar por fusión el segundo grupo de 6 cartas;
fundir los dos grupos, que ya están ordenados, tomando siempre la carta con número
menor de cualquiera de los dos grupos (que siempre será la primera de uno de los dos
grupos).
Ya ves dónde aparece la recursión, ¿no? Para ordenar 12 cartas por fusión hemos de ordenar
dos grupos de 6 cartas por fusión. Y para ordenar cada grupo de 6 cartas por fusión tendremos
que ordenar dos grupos de 3 cartas por fusión. Y para ordenar 3 grupos de cartas por fusión. . .
¿Cuándo finaliza la recursión? Cuando nos enfrentemos a casos triviales. Ordenar un grupo de
1 sola carta es trivial: ¡siempre está ordenado!
Desarrollemos un ejemplo de ordenación de un vector con 16 elementos:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

11 21 3 1 98 0 12 82 29 30 11 18 43 4 75 37

1. Empezamos separando el vector en dos ((subvectores)) de 8 elementos:


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

11 21 3 1 98 0 12 82 29 30 11 18 43 4 75 37

2. ordenamos por fusión el primer vector, con lo que obtenemos:


0 1 2 3 4 5 6 7

0 1 3 11 12 21 82 98

3. y ordenamos por fusión el segundo vector, con lo que obtenemos:


0 1 2 3 4 5 6 7

4 11 18 29 30 37 43 75

4. y ahora ((fundimos)) ambos vectores ordenados, obteniendo ası́ un único vector ordenado:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

0 1 3 4 11 11 12 18 21 29 30 37 43 75 82 98

La idea básica de la fusión es sencilla: se recorren ambos vectores de izquierda a derecha,


seleccionando en cada momento el menor elemento posible. Los detalles del proceso de
fusión son un tanto escabrosos, ası́ que lo estudiaremos con calma un poco más adelante.
Podemos representar el proceso realizado con esta imagen gráfica:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

11 21 3 1 98 0 12 82 29 30 11 18 43 4 75 37

dividir el problema (de talla 16)


en dos problemas (de talla 8),
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

11 21 3 1 98 0 12 82 29 30 11 18 43 4 75 37

resolver independientemente cada problema

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

0 1 3 11 12 21 82 98 4 11 18 29 30 37 43 75

y combinar ambas soluciones.


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

0 1 3 4 11 11 12 18 21 29 30 37 43 75 82 98

190 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

Está claro que hemos hecho ((trampa)): las lı́neas de trazo discontinuo esconden un proceso
complejo, pues la ordenación de cada uno de los vectores de 8 elementos supone la ordenación
(recursiva) de dos vectores de 4 elementos, que a su vez. . . ¿Cuándo acaba el proceso recursivo?
Cuando llegamos a un caso trivial: la ordenación de un vector que sólo tenga 1 elemento.
He aquı́ el proceso completo:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

11 21 3 1 98 0 12 82 29 30 11 18 43 4 75 37

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

11 21 3 1 98 0 12 82 29 30 11 18 43 4 75 37
Divisiones

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

11 21 3 1 98 0 12 82 29 30 11 18 43 4 75 37

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

11 21 3 1 98 0 12 82 29 30 11 18 43 4 75 37

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

11 21 3 1 98 0 12 82 29 30 11 18 43 4 75 37

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

11 21 1 3 0 98 12 82 29 30 11 18 4 43 37 75

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

1 3 11 21 0 12 82 98 11 18 29 30 4 37 43 75
Fusiones

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

0 1 3 11 12 21 82 98 4 11 18 29 30 37 43 75

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

0 1 3 4 11 11 12 18 21 29 30 37 43 75 82 98

Nos queda por estudiar con detalle el proceso de fusión. Desarrollemos primero una función
que recoja la idea básica de la ordenación por fusión: se llamará mergesort y recibirá un vector
v y, en principio, la talla del vector que deseamos ordenar. Esta función utilizará una función
auxiliar merge encargada de efectuar la fusión 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ás adelante el desarrollo de merge. De momento, el principal problema es


cómo expresar lo de ((la primera mitad de v)) y ((la segunda mitad de v)). Fı́jate: en el fondo,
se trata de señalar una serie de elementos consecutivos del vector v. Cuando ordenábamos el
vector del ejemplo tenı́amos:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

11 21 3 1 98 0 12 82 29 30 11 18 43 4 75 37

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ón a la Programación con C 191


3.6 Recursión 2004/02/10-16:33

trabaje con ((subvectores)), es decir, con un vector e ı́ndices que señalan dónde empieza y dónde
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ón merge. Dicha función 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áfico muestra cómo 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:

0 1 2 3 0 1 2 3 0 1 2 3 4 5 6 7

1 3 11 21 0 12 82 98

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én la variable k:

0 1 2 3 0 1 2 3 0 1 2 3 4 5 6 7

1 3 11 21 0 12 82 98 0

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ágenes:

0 1 2 3 0 1 2 3 0 1 2 3 4 5 6 7

1 3 11 21 0 12 82 98 0 1

i j k
0 1 2 3 0 1 2 3 0 1 2 3 4 5 6 7

1 3 11 21 0 12 82 98 0 1 3

i j k
0 1 2 3 0 1 2 3 0 1 2 3 4 5 6 7

1 3 11 21 0 12 82 98 0 1 3 11

i j k
0 1 2 3 0 1 2 3 0 1 2 3 4 5 6 7

1 3 11 21 0 12 82 98 0 1 3 11 12

i j k
0 1 2 3 0 1 2 3 0 1 2 3 4 5 6 7

1 3 11 21 0 12 82 98 0 1 3 11 12 21

i j k

Ahora, basta con copiar los últimos elementos del otro vector al final de c:

192 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

0 1 2 3 0 1 2 3 0 1 2 3 4 5 6 7

1 3 11 21 0 12 82 98 0 1 3 11 12 21 82

i j k
0 1 2 3 0 1 2 3 0 1 2 3 4 5 6 7

1 3 11 21 0 12 82 98 0 1 3 11 12 21 82 98

i j k

Un último paso del proceso de fusión 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ón. No trabajamos con dos vectores independientes,
sino con un sólo vector en el que se marcan ((subvectores)) con pares de ı́ndices.

1 void merge(int v[], int inicio1, int final 1, int inicio2, int final 2)
2 {
3 int i, j, k;
4 int c[final 2-inicio1+1]; // Vector de talla determinada en tiempo de ejecución.
5

6 i = inicio1;
7 j = inicio2;
8 k = 0;
9

10 while (i<=final 1 && j<=final 2)


11 if (v[i] < v[j])
12 c[k++] = v[i++];
13 else
14 c[k++] = v[j++];
15

16 while (i<=final 1)
17 c[k++] = v[i++];
18

19 while (j<=final 2)
20 c[k++] = v[j++];
21

22 for (k=0; k<final 2-inicio1+1; k++)


23 v[inicio1+k] = c[k];
24 }

El último paso del procedimiento se encarga de copiar los elementos de c en el vector original.
Ya está. Bueno, aún podemos efectuar una mejora para reducir el número de parámetros:
fı́jate en que inicio2 siempre es igual a final 1+1. Podemos prescindir de uno de los dos parámetros:

1 void merge(int v[], int inicio1, int final 1, int final 2)


2 {
3 int i, j, k;
4 int c[final 2-inicio1+1];
5

6 i = inicio1;
7 j = final 1+1 ;
8 k = 0;
9

10 while (i<=final 1 && j<=final 2)


11 if (v[i] < v[j])
12 c[k++] = v[i++];
13 else
14 c[k++] = v[j++];
15

16 while (i<=final 1)
17 c[k++] = v[i++];
18

19 while (j<=final 2)
20 c[k++] = v[j++];

Introducción a la Programación con C 193


3.6 Recursión 2004/02/10-16:33

21

22 for (k=0; k<final 2-inicio1+1; k++)


23 v[inicio1+k] = c[k];
24 }

Veamos cómo 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 final 1, int final 2)


6 {
7 int i, j, k;
8 int c[final 2-inicio1+1];
9

10 i = inicio1;
11 j = final 1+1;
12 k = 0;
13

14 while (i<=final 1 && j<=final 2)


15 if (v[i] < v[j])
16 c[k++] = v[i++];
17 else
18 c[k++] = v[j++];
19

20 while (i<=final 1)
21 c[k++] = v[i++];
22

23 while (j<=final 2)
24 c[k++] = v[j++];
25

26 for (k=0; k<final 2-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

194 Introducción a la Programación con C



c 2003 Andrés 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ı́ uan ejecución 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ón de la función merge:

1 void merge(int v[], int inicio1, int final 1, int final 2)


2 {
3 int i, j, k;
4 int c[final 2-inicio1+1];
5

6 for (i=inicio1 , j=final 1+1, k=0 ; i<=final 1 && j<=final 2; )


7 c[k++] = (v[i] < v[j]) ? v[i++] : v[j++];
8 while (i<=final 1) c[k++] = v[i++];
9 while (j<=final 2) c[k++] = v[j++];
10 for (k=0; k<final 2-inicio1+1; k++) v[inicio1+k] = c[k];
11 }

Observa que los bucles for aceptan más de una inicialización (separándolas por comas)
y permiten que alguno de sus elementos esté en blanco (en el primer for la acción de
incremento del ı́ndice en blanco). No te sugerimos que hagas tú lo mismo: te prevenimos
para que estés preparado cuando te enfrentes a la lectura de programas C escritos por otros.
También 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áctica frecuente y da lugar, una vez acostumbrado, a programas bastante legibles.

3.6.2. Recursión indirecta y declaración anticipada


C debe conocer la cabecera de una función antes de que sea llamada, es decir, debe conocer
el tipo de retorno y el número y tipo de sus parámetros. Normalmente ello no plantea ningún
problema: basta con definir la función antes de su uso, pero no siempre es posible. Imagina que
una función f necesita llamar a una función g y que g, a su vez, necesita llamar a f (recursión
indirecta). ¿Cuál ponemos delante? La solución es fácil: da igual, la que quieras, pero debes
hacer una declaración anticipada de la función que defines en segundo lugar. La declaración
anticipada no incluye el cuerpo de la función: consiste en la declaración del tipo de retorno,
identificador de función y lista de parámetros con su tipo, es decir, es un prototipo o perfil de
la función en cuestión.
Estudia este ejemplo6 :
6 El ejemplo es meramente ilustrativo: hay formas mucho más eficientes de saber si un número par o impar.

Introducción a la Programación con C 195


3.7 Macros 2004/02/10-16:33

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ón anticipada de la función impar , pues se usa antes de haber
sido definida. Con la declaración anticipada hemos ((adelantado)) la información acerca de qué
tipo de valores aceptará y devolverá la función.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 211 Dibuja el estado de la pila cuando se llega al caso base en la llamada recursiva impar (7).
.............................................................................................
La declaración anticipada resulta necesaria para programas con recursión indirecta, pero
también la encontrarás (o usarás) en programas sin recursión. A veces conviene definir funciones
en un orden que facilite la lectura del programa, y es fácil que se defina una función después
de su primer uso. Pongamos por caso el programa ordena.c en el que hemos implementado
el método de ordenación por fusión: puede que resulte más legible definir primero mergesort y
después 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 final 1, int final 2) ; // Declaración 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 final 1, int final 2) // 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ámetros y se usa como una función cualquiera, pero las llamadas
no se traducen en verdaderas llamadas a función. Ahora verás por qué.
Vamos con un ejemplo:

196 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

Prototipo por defecto y declaración anticipada


Si usas una función antes de definirla y no has preparado una declaración anticipada, C
deduce el tipo de cada parámetro 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ún 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ón, detecta un conflicto.
El problema se soluciona alterando el orden de definición de las funciones o, si se prefiere,
mediante una declaración 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ábamos
constantes. La diferencia está en que la macro lleva uno o más parámetros (separados por
comas) encerrados entre paréntesis. 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 %d\n", 2, CUADRADO(2));
8 return 0;
9 }

El compilador no llega a ver nunca la llamada a CUADRADO. La razón es que el preprocesador la


sustituye por su cuerpo, consiguiendo que el compilador vea esta otra versión del programa:
1 #include <stdio.h>
2

5 int main (void)


6 {
7 printf ("El cuadrado de %d es %d\n", 2, 2*2 );
8 return 0;
9 }

Las macros presentan algunas ventajas frente a las funciones:

Introducción a la Programación con C 197


3.7 Macros 2004/02/10-16:33

Por regla general, son más rápidas que las funciones, pues al no implicar una llamada
a función en tiempo de ejecución nos ahorramos la copia de argumentos en pila y el
salto/retorno a otro lugar del programa.
No obligan a dar información de tipo acerca de los parámetros ni del valor de retorno. Por
ejemplo, esta macro devuelve el máximo de dos números, sin importar que sean enteros
o flotantes:

1 #define MAXIMO(A, B) ((A > B) ? A : B)

Pero tienen serios inconvenientes:

La definición de la macro debe ocupar, en principio, una sola lı́nea. Si ocupa más de una
lı́nea, hemos de finalizar todas menos la última con el carácter ((\)) justo antes del salto
de lı́nea. Incómodo.

No puedes definir variables locales.7

No admiten recursión.
Son peligrosı́simas. ¿Qué 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 %d\n", 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é? El
preprocesador sustituye el fragmento CUADRADO(3+3) por. . . ¡3+3*3+3!
El resultado es, efectivamente, 15, y no el que esperábamos. Puedes evitar este problema
usando paréntesis:

1 #include <stdio.h>
2

3 #define CUADRADO(x) (x) * (x)


4

5 main (void)
6 {
7 printf ("El cuadrado de 6 es %d\n", 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é
esperas que calcule 1.0/CUADRADO(3+3)?, ¿el valor de 1/36, es decir, 0.02777. . . ? Te equi-
vocas. La expresión 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ón pasa por añadir nuevos paréntesis:

1 #include <stdio.h>
2

3 #define CUADRADO(x) ((x)*(x))


4

5 ...
7 No del todo cierto, pero no entraremos en detalles.

198 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

¿Ahora sı́? La expresión 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ódigo:
1 i = 3;
2 z = CUADRADO(i++);

la variable se incrementa 2 veces, y no una sóla. 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ón es poca.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 212 Diseña 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ña una macro que devuelva el mı́nimo de dos números, sin importar si son enteros
o flotantes.
· 214 Diseña una macro que calcule el valor absoluto de un número, sin importar si es entero
o flotante.
· 215 Diseña una macro que decremente una variable entera si y sólo si es positiva. La macro
devolverá el valor ya decrementado o inalterado, según 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ólo por compatibilidad con C, pero ofrecen alternativas mejores. Por ejemplo,
puedes definir funciones inline. Una función inline es como cualquier otra función, sólo que las
llamadas a ella se gestionan como las llamadas a macros: se sustituye la llamada por el código
que se ejecutarı́a en ese caso, o sea, por el cuerpo de la función con los valores que se suministren
para los parámetros. 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éntesis alrededor
de toda aparición de un parámetro.
Las funciones inline son tan útiles que compiladores como gcc las integran desde hace
años como extensión propia del lenguaje C y han pasado a formar parte del lenguaje C99. Al
compilar un programa C99 como éste:
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 ("%d\n", doble(i+1) );
14

15 return 0;
16 }

no se genera código de máquina con 10 llamadas a la función doble. El código de máquina que
se genera es virtualmente idéntico al que se genera para este otro programa equivalente:

Introducción a la Programación con C 199


3.8 Otras cuestiones acerca de las funciones 2004/02/10-16:33

1 #include <stdio.h>
2

3 int main(void)
4 {
5 int i;
6

7 for (i=0; i<10; i++)


8 printf ("%d\n", ((i+1) * 2) );
9

10 return 0;
11 }

Hay ocasiones, no obstante, en las que el compilador no puede efectuar la sustitución de


la llamada a función por su cuerpo. Si la función es recursiva, por ejemplo, la sustitución
es imposible. Pero aunque no sea recursiva, el compilador puede juzgar que una función es
excesivamente larga o compleja para que compense efectuar la sustitución. Cuando se declara
una función como inline, sólo se está sugiriendo al compilador que efectúe la sustitución, pero
éste tiene la última palabra sobre si habrá o no una verdadera llamada a función.

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ón, como cualquier otra variable local, pero recuerda su valor entre diferentes
ejecuciones de la función 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 ("%d\n", turno());
16 return 0;
17 }

Si ejecutas el programa aparecerán por pantalla los números 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ón 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úmeros primos consecutivos. Esta vez, vamos a hacerlo sin usar una variable global que re-
cuerde el valor del último 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

200 Introducción a la Programación con C



c 2003 Andrés 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úmeros primos\n");


26 for (i=0; i<10; i++)
27 printf ("%d\n", 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ón en bastantes casos.

3.8.3. Paso de funciones como parámetros


Hay un tipo de parámetro especial que puedes pasar a una función: ¡otra función!
Veamos un ejemplo. En este fragmento del programa se definen sendas funciones C que
aproximan numéricamente la integral definida en un intervalo para las funciones matemáticas
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ásicamente iguales. Sólo difieren en su identificador
y en la función matemática que integran. ¿No serı́a mejor disponer de una única función C,
digamos integra, a la que suministremos como parámetro la función matemática que queremos
integrar? C lo permite:

Introducción a la Programación con C 201


3.8 Otras cuestiones acerca de las funciones 2004/02/10-16:33

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ámetro que es de tipo puntero a función. Cuando llamamos a
integra, el cuarto parámetro puede ser el identificador de una función 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: %f\n", integra(0.0, 1.0, 10, cuadrado ));
30 printf ("Integral 2: %f\n", integra(0.0, 1.0, 10, cubo ));
31 return 0;
32 }

La forma en que se declara un parámetro del tipo ((puntero a función)) resulta un tanto
complicada. En nuestro caso, lo hemos declarado ası́: float (*f )(float). El primer float indica
que la función devuelve un valor de ese tipo. El (*f ) indica que el parámetro f es un puntero a
función. Y el float entre paréntesis indica que la función trabaja con un parámetro de tipo float.
Si hubiésemos necesitado trabajar con una función que recibe un float y un int, hubiésemos
escrito float (*f )(float, int) en la declaración del parámetro.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 216 ¿Puedes usar la función integra para calcular la integral definida de la función ma-
temática sin(x)? ¿Cómo?

202 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

· 217 Diseña una función C capaz de calcular


b
X
f (i),
i=a

siendo f una función matemática cualquiera que recibe un entero y devuelve un entero.
· 218 Diseña una función C capaz de calcular
b X
X d
f (i, j),
i=a j=c

siendo f una función matemática cualquiera que recibe dos enteros y devuelve un entero.
.............................................................................................

3.9. Módulos, bibliotecas y unidades de compilación


Cuando te enfrentas a la escritura de un programa largo, individualmente o en equipo, te
resultará virtualmente imposible escribirlo en un único fichero de texto. Resulta más práctico
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álculos.
Proceder ası́ tiene varias ventajas:
Mejora la legibilidad del código (cada fichero es relativamente breve y agrupa temáticamente
las funciones, registros y constantes).
La compilación es más rápida (cuando se modifica un fichero, sólo es necesario compilar
ese fichero).
Y, quizá lo más importante, permite reutilizar código. Es un beneficio a medio y largo pla-
zo. Si, por ejemplo, te dedicas a programar videojuegos tridimensionales, verás 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étera; funciones
que los manipulan, visualizan, leen/escriben en disco, etcétera. 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ón obligarı́a a editar
todos los programas en los que se pegó; por contra, si está en un solo fichero, basta con
corregir la definición una sola vez.
C permite escribir un programa como una colección de unidades de compilación. El con-
cepto es similar al de los módulos 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ón (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 última etapa
todas las unidades compiladas para crear un único fichero ejecutable.
Lo mejor será que aprendamos sobre unidades de compilación escribiendo una muy sencilla:
un módulo en el que se define una función que calcula el máximo de dos número enteros.
El fichero que corresponde a esta unidad de compilación se llamará 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á en otro fichero llamado principal.c. Dicho programa lla-
mará a la función maximo:

Introducción a la Programación con C 203


3.9 Módulos, bibliotecas y unidades de compilación 2004/02/10-16:33

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úmero: ");


8 scanf ("%d", &x);
9 printf ("Dame otro: ");
10 scanf ("%d", &y);
11 printf ("El máximo es %d\n", maximo(x, y) );
12 return 0;
13 }
Hemos marcado el programa como incorrecto. ¿Por qué? Verás, estamos usando una función,
maximo, que no está definida en el fichero principal.c. ¿Cómo sabe el compilador cuántos
parámetros recibe dicha función?, ¿y el tipo de cada parámetro?, ¿y el tipo del valor de retorno?
El compilador se ve obligado a generar código de máquina para llamar a una función de la que
no sabe nada. Mala cosa.
¿Cómo se resuelve el problema? Puedes declarar la función sin definirla, es decir, puedes
declarar el aspecto de su cabecera (lo que denominamos su prototipo) e indicar que es una
función 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úmero: ");


10 scanf ("%d", &x);
11 printf ("Dame otro: ");
12 scanf ("%d", &y);
13 printf ("El máximo es %d\n", maximo(x, y));
14 return 0;
15 }
El prototipo contiene toda la información útil para efectuar la llamada a la función, pero no
contiene su cuerpo: la cabecera acaba con un punto y coma. Fı́jate en que la declaración del
prototipo de la función maximo empieza con la palabra clave extern. Con ella se indica al
compilador que maximo está definida en algún módulo ((externo)). También puedes indicar con
extern que una variable se define en otro módulo.
Puedes compilar el programa ası́:

$ gcc extremos.c -c

$ gcc principal.c -c

$ gcc principal.o extremos.o -o principal

La compilación necesita tres pasos: uno por cada unidad de compilación y otro para enlazar.
1. El primer paso (gcc extremos.c -c) traduce a código de máquina el fichero o unidad de
compilación extremos.c. La opción -c indica al compilador que extremos.c es un módulo
y no define a la función main. El resultado de la compilación se deja en un fichero llamado
extremos.o. La extensión ((.o)) abrevia el término ((object code)), es decir, ((código objeto)).
Los ficheros con extensión ((.o)) contienen el código de máquina 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ólo eso: también contienen otra información, como la denominada tabla de sı́mbolos.

204 Introducción a la Programación con C



c 2003 Andrés 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ón ((.o)) y genera un único fichero ejecutable, llamado prin-
cipal . Este último paso se encarga de enlazar las dos unidades compiladas para generar
el fichero ejecutable.
Por enlazar entendemos que las llamadas a funciones cuyo código de máquina era desco-
nocido (estaba en otra unidad de compilación) se traduzcan en ((saltos)) a las direcciones
en las que se encuentran los subprogramas de código máquina correspondientes (y que
ahora se conocen).
Aquı́ tienes un diagrama que ilustra el proceso:
Paso 1
extremos.c Compilador extremos.o

Paso 3
Enlazador principal

Paso 2
principal.c Compilador principal.o

Puedes ahorrarte un paso fundiendo los dos últimos en uno sólo. Ası́:

$ gcc extremos.c -c

$ gcc principal.c extremos.o -o principal

Este diagrama muestra todos los pasos del proceso a los que aludimos:
Paso 1
extremos.c Compilador extremos.o

principal.c Compilador Enlazador principal

Paso 2

Para conseguir un programa ejecutable es necesario que uno de los módulos (¡pero sólo uno
de ellos!) defina una función main. Si ningún módulo define main o si main se define en más
de un módulo, el enlazador protestará y no generará fichero ejecutable alguno.

3.9.1. Declaración de prototipos en cabeceras


Hemos resuelto el problema de gestionar diferentes unidades de compilación, pero la solución de
tener que declarar el prototipo de cada función en toda unidad de compilación 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ódulo. Las cabeceras
son ficheros con extensión ((.h)) (es un convenio: la ((h)) es abreviatura de ((header))).
Nuestra cabecera será 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ón a la Programación con C 205


3.9 Módulos, bibliotecas y unidades de compilación 2004/02/10-16:33

Documentación y cabeceras
Es importante que documentes bien los ficheros de cabecera, pues es frecuente que los
programadores que usen tu módulo lo consulten para hacerse una idea de qué ofrece.
Nuestro módulo podrı́a haberse documentado ası́:

extremos.h
1 /*******************************************************
2 * Módulo: extremos
3 *
4 * Propósito: funciones para cálculo de valores máximos
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ón minimo.
12 *******************************************************/
14

15 extern int maximo(int a, int b);


16 /* Calcula el máximo de dos número enteros a y b. */

¿Y por qué 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é accesible. Si el módulo es un producto comercial, probablemente sólo les hayan
vendido el módulo 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ón, funciones auxiliares, variables para uso interno, etc., que hacen
engorrosa su lectura. El fichero de cabecera contiene una somera declaración de cada uno
de los elementos del módulo que se ((publican)) para su uso en otros módulos o programas,
ası́ que es una especie de resumen del .c.

8 printf ("Dame un número: ");


9 scanf ("%d", &x);
10 printf ("Dame otro: ");
11 scanf ("%d", &y);
12 printf ("El máximo es %d\n", maximo(x, y));
13 return 0;
14 }

La única 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ódulo extremos.o contuviera muchas funciones, con sólo una lı́nea habrı́amos conseguido
((importarlas)) todas.
Aquı́ tienes una actualización del gráfico que muestra el proceso completo de compilación:

extremos.c Compilador extremos.o

principal.c Preprocesador Compilador Enlazador principal

extremos.h

206 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 3 Funciones

Bibliotecas
Ya has usado funciones y datos predefinidos, como las funciones y las constantes ma-
temáticas. Hemos hablado entonces del uso de la biblioteca matemática. ¿Por qué
((biblioteca)) y no ((módulo))? Una biblioteca es más que un módulo: es un conjunto de
módulos.
Cuando se tiene una pléyade de ficheros con extensión ((.o)), conviene empaquetarlos en
uno solo con extensión ((.a)) (por ((archive))). Los ficheros con extensión ((.a)) son similares
a los ficheros con extensión ((.tar)): meras colecciones de ficheros. De hecho, ((tar)) (tape
archiver ) es una evolución de ((.ar)) (por ((archiver))), el programa con el que se manipulan
los ficheros con extensión ((.a)).
La biblioteca matemática, por ejemplo, agrupa un montón de módulos. 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ón ((.o)) en su interior. (Sólo 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ática compilas ası́:

$ gcc programa.c -lm -o programa

o, equivalentemente, ası́:

$ gcc programa.c /usr/lib/libm.a

En el segundo caso hacemos explı́cito el nombre de la biblioteca en la que se encuentran


las funciones matemáticas. El enlazador no sólo sabe tratar ficheros con extensión ((.o)):
también sabe buscarlos en los de extensión ((.a)).
En cualquier caso, sigue siendo necesario que las unidades de compilación conozcan el
perfil de las funciones que usan y están definidas en otros módulos o bibliotecas. Por eso
incluı́mos, cuando conviene, el fichero math.h en nuestros programas.
Hay infinidad de bibliotecas que agrupan módulos con utilidades para diferentes campos
de aplicación: resolución de problemas matemáticos, diseño de videojuegos, reproducción
de música, etc. Algunas son código abierto, en cuyo caso se distribuyen con los ficheros de
extensión ((.c)), los ficheros de extensión ((.h)) y alguna utilidad para facilitar la compilación
(un makefile). Cuando son comerciales es frecuente que se mantenga el código fuente en
privado. En tal caso, se distribuye el fichero con extensión ((.a)) (o una colección de ficheros
con extensión ((.o))) y uno o más ficheros con extensión ((.h)).

3.9.2. Declaración de variables en cabeceras


No sólo puedes declarar funciones en los ficheros de cabecera. También 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ódulo, por ejemplo,
declara una variable entera en mimodulo.c:
mimodulo.c
1 int variable;

Si deseamos que otras unidades de compilación puedan acceder a esa variable, tendremos que
incluir su declaración en la cabecera. ¿Cómo? Una primera idea es poner, directamente, la
declaración ası́:
E mimodulo.h E
1 int variable;

Introducción a la Programación con C 207


3.9 Módulos, bibliotecas y unidades de compilación 2004/02/10-16:33

Pero es incorrecta. El problema radica en que cuando incluyamos la cabecera mimodulo.h en


nuestro programa, se insertará la lı́nea int variable;, sin más, ası́ que se estará 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á que hay dos objetos que tienen el mismo identificador, y eso está prohibido. La solución es
sencilla: preceder la declaración 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á definida en alguna unidad de compilación, por lo que no
la crea por segunda vez.

3.9.3. Declaración de registros en cabeceras


Finalmente, puedes declarar también registros en las cabeceras. Como los programas que cons-
truiremos son sencillos, no se planteará problema alguno con la definición de registros: basta con
que pongas su declaración en la cabecera, sin más. 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á 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:

208 Introducción a la Programación con C



c 2003 Andrés 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ón 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ódigo condicional)). Se interpretan ası́:
((si la constante A_H no está definida, entonces incluye el fragmento hasta el #endif , en caso
contrario, sáltate el texto hasta el #endif )). O sea, el compilador verá o no lo que hay entre
las lı́neas 1 y 6 en función 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ón de que se cumpla o no una condición en tiempo de ejecución. Las
directivas presentadas permiten que el compilador vea o no un fragmento arbitrario de texto
en función de si existe o no una constante en tiempo de compilación.
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ún definida A_H, ası́ que
se incluirán las lı́neas 3–8. Uno de los efectos será que A_H pasará a estar definida. La segunda
vez que se incluya la cabecera a.h, A_H ya estará definida, ası́ que el compilador no verá por
segunda vez la definición de struct A.
El efecto final es que la definición de struct A sólo 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ón a la Programación con C 209


3.9 Módulos, bibliotecas y unidades de compilación 2004/02/10-16:33

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ón de a.h no ha supuesto el copiado del texto guardado entre directivas
#ifndef /#endif . Ingenioso, ¿no?

210 Introducción a la Programación con C


Capı́tulo 4

Estructuras de datos: memoria


dinámica

La Reina se puso congestionada de furia, y, tras lanzarle una mirada felina, empezó 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ño debı́a ser fijo y conocido en tiempo de compilación, es decir, no
podı́amos alargar o acortar los vectores para que se adaptaran al tamaño de una serie de datos
durante la ejecución del programa. C permite una gestión dinámica de la memoria, es decir,
solicitar memoria para albergar el contenido de estructuras de datos cuyo tamaño exacto no
conocemos hasta que se ha iniciado la ejecución del programa. Estudiaremos aquı́ dos formas
de superar las limitaciones de tamaño que impone el C:
mediante vectores cuyo tamaño se fija en tiempo de ejecución,
y mediante registros enlazados, también 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ámicos


Sabemos definir vectores indicando su tamaño en tiempo de compilación:
1 #define TALLA 10
2

3 int a[TALLA];

Pero, ¿y si no sabemos a priori cuántos elementos debe albergar el vector?1 Por lo estudiado
hasta el momento, podemos definir TALLA como el número más grande de elementos posible,
el número de elementos para el peor de los casos. Pero, ¿y si no podemos determinar un
número máximo de elementos? Aunque pudiéramos, ¿y si éste fuera tan grande que, en la
práctica, supusiera un despilfarro de memoria intolerable para situaciones normales? Imagina
una aplicación de agenda telefónica personal que, por si acaso, reserva 100000 entradas en un
vector. Lo más probable es que un usuario convencional no gaste más 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ñaran ası́, la memoria
disponible se agotarı́a rapidı́simamente.
1 En la sección 3.5.3 vimos cómo definir vectores locales cuya talla se decide al ejecutar una función: 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ño cuantas veces
queramos. Los ((vectores de longitud variable)) que estudiamos en su momento son inapropiados para cualquiera
de estos dos objetivos.

Introducción a la Programación con C 211


4.1 Vectores dinámicos 2004/02/10-16:33

4.1.1. malloc, free y NULL


Afortunadamente, podemos definir, durante la ejecución del programa, vectores cuyo tamaño
es exactamente el que el usuario necesita. Utilizaremos para ello dos funciones de la biblioteca
estándar (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ño que se indique (en bytes);

free (que en inglés significa ((liberar))): libera memoria obtenida con malloc, es decir, la
marca como disponible para futuras llamadas a malloc.

Para hacernos una idea de cómo 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úmero 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ómo se ha definido el vector a (lı́nea 6): como int * a, es decir, como puntero a entero.
No te dejes engañar: 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ámico de enteros, pues su memoria se obtiene dinámicamente,
esto es, en tiempo de ejecución y según convenga a las necesidades. No sabemos aún cuántos
enteros serán apuntados por a, ya que el valor de talla no se conocerá 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ón de
memoria en la que empiezan esos enteros. La función malloc presenta un prototipo similar a
éste:

stdlib.h
...
void * malloc(int bytes);
...

Es una función que devuelve un puntero especial, del tipo de datos void *. ¿Qué significa
void *? Significa ((puntero a cualquier tipo de datos)), o sea, ((dirección de memoria)), sin más.
La función malloc no se usa sólo para reservar vectores dinámicos de enteros: puedes reservar
con ella vectores dinámicos de cualquier tipo base. Analicemos ahora el argumento que pasamos
a malloc. La función espera recibir como argumento un número entero: el número 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ón 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á un total de 20 bytes y la
memoria quedará ası́ tras ejecutar la lı́nea 10:
0 1 2 3 4

212 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 4 Estructuras de datos: memoria dinámica

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ámico, no hay diferencia alguna entre él y un
vector estático desde el punto de vista práctico. Ambos pueden indexarse (lı́nea 12) o pasarse
como argumento a funciones que admiten un vector del mismo tipo base.

Aritmética 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ón a con un desplazamiento de i veces el tamaño del tipo base)). La sentencia de
asignación 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ño del tipo base)) (en el ejemplo, 4 bytes, que es el tamaño de un int). Es
lo que se conoce por aritmética de punteros.
La aritmética de punteros es un punto fuerte de C, aunque también 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ón free tiene un prototipo similar a éste:
stdlib.h
...
void free(void * puntero);
...

Como ves, free recibe un puntero a cualquier tipo de datos: la dirección 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 óptima.
Recuerda: tu programa debe efectuar una llamada a free por cada llamada a malloc. Es muy
importante.
Conviene que después 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áficamente, 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ón 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é? 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és 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ón malloc puede fallar por diferentes motivos. Podemos saber cuándo ha fallado
porque malloc lo notifica devolviendo el valor NULL. Imagina que solicitas 2 megabytes de
memoria en un ordenador que sólo dispone de 1 megabyte. En tal caso, la función malloc
devolverá el valor NULL para indicar que no pudo efectuar la reserva de memoria solicitada.
2 Y, como en el caso de un fichero, si no lo liberas tú explı́citamente, se libera automáticamente al finalizar

la ejecución del programa. Aún ası́, te exigimos disciplina: oblı́gate a liberarlo tú mismo tan pronto dejes de
necesitarlo.

Introducción a la Programación con C 213


4.1 Vectores dinámicos 2004/02/10-16:33

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 suficiente\n");
4 }
5 else {
6 ...
7 }

Es posible (y una forma de expresión idiomática de C) solicitar la memoria y comprobar si se


pudo obtener en una única lı́nea (presta atención al uso de paréntesis, es importante):
1 if ( (a = malloc(talla * sizeof (int))) == NULL) {
2 printf ("Error: no hay memoria suficiente\n");
3 }
4 else {
5 ...
6 }

Nuestros programas, sin embargo, no incluirán esta comprobación. Estamos aprendiendo a pro-
gramar y sacrificaremos las comprobaciones como ésta 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ón de la memoria
Ya hemos dicho que malloc puede fracasar si se solicita más memoria de la disponible en
el ordenador. Parece lógico pensar que en un ordenador con 64 megabytes, de los que el
sistema operativo y los programas en ejecución han consumido, digamos, 16 megabytes,
podamos solicitar un bloque de hasta 48 megabytes. Pero eso no está garantizado. Imagina
que los 16 megabytes ya ocupados no están 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á ocupado y tres están libres, como muestra esta figura:

En tal caso, el bloque de memoria más grande que podemos obtener con malloc es de ¡sólo
tres megabytes!
Decimos que la memoria está fragmentada para referirnos a la alternancia de bloques
libres y ocupados que limita su disponibilidad. La fragmentación no sólo limita el máximo
tamaño de bloque que puedes solicitar, además, afecta a la eficiencia con la que se ejecutan
las llamadas a malloc y free.

También 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úmero 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

214 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 4 Estructuras de datos: memoria dinámica

16 return 0;
17 }

Aritmética de punteros y recorrido de vectores


La aritmética de punteros da lugar a expresiones idiomáticas 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úmero 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áctica lo aprendido desarrollando un par de ejemplos.

Creación de un nuevo vector con una selección, de talla desconocida, de elementos


de otro vector
Empezaremos por diseñar una función que recibe un vector de enteros, selecciona aquellos cuyo
valor es par y los devuelve en un nuevo vector cuya memoria se solicita dinámicamente.
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ántos 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ón a la Programación con C 215


4.1 Vectores dinámicos 2004/02/10-16:33

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ón.
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úa ahora la selección 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ántos elementos pares hay?
44 for (i=0; i< ???? ; i++)
45 printf ("%d\n", seleccion[i]);
46

47 free(seleccion);
48 seleccion = NULL;
49

50 return 0;
51 }

Tenemos un problema al usar selecciona_pares: no sabemos cuántos valores ha seleccionado.


Podemos modificar la función para que modifique el valor de un parámetro 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úmero de elementos pares en el parámetro 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];

216 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 4 Estructuras de datos: memoria dinámica

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úmero de elementos pares en el parámetro 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úa ahora la selección 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ás, la variable seleccionados contiene el número de pares.
42

43 // Ahora los mostramos en pantalla.


44 for (i=0; i< seleccionados ; i++)
45 printf ("%d\n", seleccion[i]);
46

47 free(seleccion);
48 seleccion = NULL;
49

50 return 0;
51 }
Por cierto, el prototipo de la función, que es éste:
int * selecciona_pares( int a[] , int talla, int * seleccionados);
puede cambiarse por este otro:
int * selecciona_pares( int * a , int talla, int * seleccionados);

Introducción a la Programación con C 217


4.1 Vectores dinámicos 2004/02/10-16:33

Conceptualmente, es lo mismo un parámetro 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ámetro es un vector de enteros, ya que ası́ lo distinguimos fácilmente de un entero pasado
por referencia. Si ves el último prototipo, no hay nada que te permita saber si a es un vector o
un entero pasado por referencia como seleccionados. Es más 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ón te parezca correcta:

int * primeros(void)
{
int i, v[10];
for (i=0; i<10; i++)
v[i] = i + 1;
return v;
}

La función devuelve, a fin de cuentas, una dirección de memoria en la que empieza una
secuencia de enteros. Y es verdad: eso es lo hace. El problema radica en que la memoria a
la que apunta ¡no ((existe)) fuera de la función! La memoria que ocupa v se libera tan pronto
finaliza la ejecución de la función. Este intento de uso de la función, 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, éste no puede apuntar a datos locales.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 219 Diseña una función que seleccione todos los números positivos de un vector de enteros.
La función recibirá el vector original y un parámetro con su longitud y devolverá dos datos: un
puntero al nuevo vector de enteros positivos y su longitud. El puntero se devolverá como valor
de retorno de la función, y la longitud mediante un parámetro adicional (un entero pasado por
referencia).
· 220 Desarrolla una función que seleccione todos los números de un vector de float mayores
que un valor dado. Diseña un programa que llame correctamente a la función y muestre por
pantalla el resultado.
· 221 Escribe un programa que lea por teclado un vector de float cuyo tamaño se solicitará
previamente al usuario. Una vez leı́dos los componentes del vector, el programa copiará sus
valores en otro vector distinto que ordenará con el método de la burbuja. Recuerda liberar toda
memoria dinámica solicitada antes de finalizar el programa.
· 222 Escribe una función que lea por teclado un vector de float cuyo tamaño se solicitará
previamente al usuario. Escribe, además, una función que reciba un vector como el leı́do en la
función anterior y devuelva una copia suya con los mismos valores, pero ordenados de menor a
mayor (usa el método de ordenación de la burbuja o cualquier otro que conozcas).
Diseña un programa que haga uso de ambas funciones. Recuerda que debes liberar toda
memoria dinámica solicitada antes de finalizar la ejecución del programa.
· 223 Escribe una función que reciba un vector de enteros y devuelva otro con sus n mayores
valores, siendo n un número menor o igual que la talla del vector original.
3 En realidad, hay una pequeña diferencia. La declaración int a[] hace que a sea un puntero inmutable,

mientras que int * a permite modificar la dirección apuntada por a haciendo, por ejemplo, a++. De todos
modos, no haremos uso de esa diferencia en este texto.

218 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 4 Estructuras de datos: memoria dinámica

· 224 Escribe una función que reciba un vector de enteros y un valor n. Si n es menor o igual
que la talla del vector, la función devolverá el un vector con las n primeras celdas del vector
original. En caso contrario, devolverá 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ón devuelva valores mediante return y, a la vez, me-
diante parámetros pasados por referencia. Una posibilidad es usar únicamente 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 ("%d\n", seleccion[i]);
37

38 free(seleccion);
39 seleccion = NULL;
40

41 return 0;
42 }

Fı́jate en la declaración del parámetro pares en la lı́nea 7: es un puntero a un vector de


enteros, o sea, un vector de enteros cuya dirección se suministra a la función. ¿Por qué? Porque
a resultas de llamar a la función, la dirección apuntada por pares será una ((nueva)) dirección (la
que obtengamos mediante una llamada a malloc). La lı́nea 16 asigna un valor a *pares. Resulta
interesante que veas cómo se asigna valores al vector apuntado por *pares en la lı́nea 21 (los
paréntesis 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ón 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ón de memoria de un puntero
de enteros. La cabecera de la función selecciona_pares podrı́a haberse definido ası́:

Introducción a la Programación con C 219


4.1 Vectores dinámicos 2004/02/10-16:33

void selecciona_pares(int a[], int talla, int ** pares , int * numpares)

¿Ves cómo 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ón malloc, por ejemplo, devuelve el valor NULL cuando
no consigue reservar la memoria solicitada y un valor diferente cuando sı́ lo consigue. La
función scanf , que hemos estudiado como si no devolviese valor alguno, sı́ lo hace: devuelve
el número de elementos cuyo valor ha sido efectivamente leı́do. Si, por ejemplo, llamamos
a scanf ("%d %d", &a, &b), la función devuelve el valor 2 si todo fue bien (se leyó el
contenido de dos variables). Si devuelve el valor 1, es porque sólo consiguió leer el valor de
a, y si devuelve el valor 0, no consiguió leer ninguno de los dos. Un programa robusto debe
comprobar el valor devuelto siempre que se efectúe 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ón normal.
5 ...
6 }

Las rutinas que nosotros diseñamos deberı́an presentar un comportamiento similar. La fun-
ción 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ón:

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ón más modernos, como C++ o Python, suelen basar la detección
(y el tratamiento) de errores en las denominadas ((excepciones)).

Más elegante resulta definir un registro ((vector dinámico de enteros)) que almacene con-
juntamente tanto el vector de de elementos propiamente dicho como el tamaño del vector4 :
4 Aunque recomendemos este nuevo método para gestionar vectores de tamaño variable, has de saber, cuando

menos, leer e interpretar correctamente parámetros con tipos como int a[], int *a, int *a[] o int **a, pues
muchas veces tendrás que utilizar bibliotecas escritas por otros programadores o leer código fuente de programas
cuyos diseñadores optaron por estos estilos de paso de parámetros.

220 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 4 Estructuras de datos: memoria dinámica

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úmero de enteros almacenados en esa zona de memoria.
8 };
9

10 struct VectorDinamicoEnteros selecciona_pares(struct VectorDinamicoEnteros entrada)


11 // Recibe un vector dinámico y devuelve otro con una selección 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 (pares.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 ("%d\n", seleccionados.elementos[i]);
47

48 free(seleccionados.elementos);
49 seleccionados.elementos = NULL;
50 seleccionados.talla = 0;
51

52 return 0;
53 }

El único problema de esta aproximación es la potencial fuente de ineficiencia que supone


devolver una copia de un registro, pues podrı́a ser de gran tamaño. No es nuestro caso: un
struct VectorDinamicoEnteros ocupa sólo 8 bytes. Si el tamaño fuera un problema, podrı́amos
usar una variable de ese tipo como parámetro pasado por referencia. Usarı́amos ası́ sólo 4 bytes:

pares 3.c pares.c

Introducción a la Programación con C 221


4.1 Vectores dinámicos 2004/02/10-16:33

1 #include <stdio.h>
2 #include <stdlib.h>
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 ("%d\n", 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écnicamente diferentes para realizar lo mismo. Deberás
elegir en función de la elegancia de cada solución y de su eficiencia.

Representación de polı́gonos con un número arbitrario de vértices


Desarrollemos un ejemplo más: un programa que lea los vértices 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;

222 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 4 Estructuras de datos: memoria dinámica

Listas Python
Empieza a quedar claro que Python es un lenguaje mucho más cómodo que C para gestionar
vectores dinámicos, que allı́ denominábamos listas. No obstante, debes tener presente que
el intérprete de Python está escrito en C, ası́ que cuando manejas listas Python estás,
indirectamente, usando memoria dinámica como malloc y free.
Cuando creas una lista Python con una orden como a = [0] * 5 o a = [0, 0, 0, 0, 0],
estás reservando espacio en memoria para 5 elementos y asignándole 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ás complejo).
Cuando se pierde la referencia a una lista (por ejemplo, cambiando el valor asignado
a a), Python se encarga de detectar automáticamente 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.

};

Fı́jate en que un polı́gono presenta un número de puntos inicialmente desconocido, por


lo que hemos de recurrir a memoria dinámica. Reservaremos la memoria justa para guardar
dichos puntos en el campo p (un puntero a una secuencia de puntos) y el número de puntos se
almacenará en el campo puntos.
Aquı́ tienes una función 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úmero 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 %d\n", 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á bien una función 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ón 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) );

Introducción a la Programación con C 223


4.1 Vectores dinámicos 2004/02/10-16:33

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 }

Es importante que entiendas bien expresiones como pol.p[i].x. Esa, en particular, significa: del
parámetro 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ólo nos
interesa acceder a su campo x (que, por cierto, es de tipo float).
Juntemos todas las piezas y añadamos 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úmero 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 %d\n", 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;

224 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 4 Estructuras de datos: memoria dinámica

52

53 un_poligono = lee_poligono();
54 perimetro = perimetro_poligono(un_poligono);
55 printf ("Perı́metro %f\n", perimetro);
56 libera_poligono(&un_poligono);
57

58 return 0;
59 }

No es el único modo en que podrı́amos haber escrito el programa. Te presentamos ahora


una implementación con bastantes diferencias en el modo de paso de parámetros:
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úmero 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 %d\n", 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

Introducción a la Programación con C 225


4.1 Vectores dinámicos 2004/02/10-16:33

52 lee_poligono(&un_poligono);
53 perimetro = perimetro_poligono(&un_poligono);
54 printf ("Perı́metro %f\n", perimetro);
55 libera_poligono(&un_poligono);
56

57 return 0;
58 }

En esta versión hemos optado, siempre que ha sido posible, por el paso de parámetros por
referencia, es decir, por pasar la dirección de la variable en lugar de una copia de su contenido.
Hay una razón 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ón de
memoria, sólo hay que pasar 4 bytes. En este caso particular no hay una ganancia extraordinaria,
pero en otras aplicaciones manejarás structs tan grandes que el paso de la dirección compensará
la ligera molestia de la notación de acceso a campos con el operador ->.
Puede que te extrañe el término const calificando el parámetro de perimetro_poligono. Su
uso es opcional y sirve para indicar que, aunque es posible modificar la información apuntada por
pol , no lo haremos. En realidad suministramos el puntero por cuestión de eficiencia, no porque
deseemos modificar el contenido. Con esta indicación conseguimos dos efectos: si intentásemos
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ón apuntada por
pol pudiera modificarse en la función.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 225 ¿Funciona esta otra implementación 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ña una función que cree un polı́gono regular de n lados inscrito en una circunfe-
rencia de radio r. Esta figura muestra un pentágono inscrito en una circunferencia de radio r y
las coordenadas de cada uno de sus vértices:
(r cos(2π/5), r sin(2π/5))

(r cos(2π · 2/5), r sin(2π · 2/5))

r
(r cos(0), r sin(0))

(r cos(2π · 3/5), r sin(2π · 3/5))

(r cos(2π · 4/5), r sin(2π · 4/5))

Utiliza la función para crear polı́gonos regulares de talla 3, 4, 5, 6, . . . inscritos en una


circunferencia de radio 1. Calcula a continuación el perı́metro de los sucesivos polı́gonos y
comprueba si dicho valor se aproxima a 2π.
· 227 Diseña un programa que permita manipular polinomios de cualquier grado. Un poli-
nomio se representará con el siguiente tipo de registro:
1 struct Polinomio {
2 float * p;
3 int grado;
4 };

226 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 4 Estructuras de datos: memoria dinámica

Como puedes ver, el campo p es un puntero a float, o sea, un vector dinámico de float. Diseña
y utiliza funciones que hagan lo siguiente:

Leer un polinomio por teclado. Se pedirá el grado del polinomio y, tras reservar memoria
suficiente para sus coeficientes, se pedirá también 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á, en principio, grado igual que el mayor grado de los operandos. (Hay
excepciones; piensa cuáles.)

Multiplicar dos polinomios.

· 228 Diseña un programa que solicite la talla de una serie de valores enteros y dichos valores.
El programa ordenará a continuación los valores mediante el procedimiento mergesort. (Ten en
cuenta que el vector auxiliar que necesita merge debe tener capacidad para el mismo número
de elementos que el vector original.)
.............................................................................................

Reserva con inicialización automática


La función calloc es similar a malloc, pero presenta un prototipo diferente y hace algo más
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ámetro es el número de elementos y el segundo, el número 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ásemos este fragmento
de código:

a = malloc( talla * sizeof (int) );


for (i = 0; i < talla; i++) a[i] = 0;

¿Por qué no usar siempre calloc, si parece mejor que malloc? Por eficiencia. En ocasiones
no desearás que se pierda tiempo de ejecución inicializando la memoria a cero, ya que tú
mismo querrás inicializarla a otros valores inmediatamente. Recuerda que garantizar la mayor
eficiencia de los programas es uno de los objetivos del lenguaje de programación C.

4.1.3. Cadenas dinámicas


Las cadenas son un caso particular de vector. Podemos usar cadenas de cualquier longitud
gracias a la gestión de memoria dinámica. Este programa, por ejemplo, lee dos cadenas y
construye una nueva que resulta de concatenar a éstas.

cadenas dinamicas.c cadenas dinamicas.c


1 #include <stdio.h>
2 #include <string.h>
3

4 #define CAPACIDAD 80
5

6 int main(void)
7 {
8 char cadena1[CAPACIDAD+1], cadena2[CAPACIDAD+1];
9 char * cadena3;
10

11 printf ("Dame un texto: "); gets(cadena1);


12 printf ("Dame otro texto: "); gets(cadena2);

Introducción a la Programación con C 227


4.2 Matrices dinámicas 2004/02/10-16:33

13

14 cadena3 = malloc( (strlen(cadena1) + strlen(cadena2) + 1) * sizeof (char) );


15

16 strcpy(cadena3, cadena1);
17 strcat(cadena3, cadena2);
18

19 printf ("Resultado de concatenar ambos: %s\n", cadena3);


20

21 free(cadena3);
22 cadena3 = NULL;
23

24 return 0;
25 }

Como las dos primeras cadenas se leen con gets, hemos de definirlas como cadenas estáticas.
La tercera cadena reserva exactamente la misma cantidad de memoria que ocupa.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 229 Diseña una función que lea una cadena y construya otra con una copia invertida de
la primera. La segunda cadena reservará sólo la memoria que necesite.
· 230 Diseña una función que lea una cadena y construya otra que contenga un ejemplar de
cada carácter de la primera. Por ejemplo, si la primera cadena es "este ejemplo", la segunda
será "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ón. ¿Por qué? Porque los literales de
cadena ((residen)) en una zona de memoria especial (la denominada ((zona de texto))) que
está protegida contra escritura. Y hay una razón para ello: en esa zona reside, también,
el código de máquina correspondiente al programa. Que un programa modifique su propio
código de máquina es una pésima práctica (que era relativamente frecuente en los tiempos
en que predominaba la programación en ensamblador), hasta el punto de que su zona de
memoria se marca como de sólo lectura.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 231 Implementa una función 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ámetro 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ámicas


Podemos extender la idea de los vectores dinámicos a matrices dinámicas. Pero el asunto se com-
plica notablemente: no podemos gestionar la matriz como una sucesión de elementos contiguos,
sino como un ((vector dinámico de vectores dinámicos)).

4.2.1. Gestión de memoria para matrices dinámicas


Analiza detenidamente este programa:

228 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 4 Estructuras de datos: memoria dinámica

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ón 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ón del tipo

Empecemos por la declaración de la matriz (lı́nea 6). Es un puntero un poco extraño: 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ámico de vectores dinámicos 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ón de memoria. Se solicita espacio para un número filas de punteros
a float. Supongamos que filas vale 4. Tras esa petición, tenemos la siguiente asignación de
memoria para m:

0
m
1

El vector m es un vector dinámico 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á 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ón a la Programación con C 229


4.2 Matrices dinámicas 2004/02/10-16:33

0 1 2 3 4
0
m
0 1 2 3 4
1

0 1 2 3 4
2

0 1 2 3 4
3

Acceso a filas y elementos


Bien. ¿Y cómo se usa m ahora? ¡Como cualquier matriz! Pensemos en qué 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ámico a elementos del tipo float *. El
elemento m[1] es el segundo componente de m. ¿Y de qué tipo es? De tipo float *, un nuevo
puntero o vector dinámico, pero a valores de tipo float. Si es un vector dinámico, lo podemos
indexar, ası́ que es válido escribir m[1][2]. ¿Y de qué 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ón, ((desaparece)) un asterisco del tipo de datos.

Liberación de memoria: un free para cada malloc


Sigamos con el programa. Nos resta la liberación 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ásemos 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ón de memoria incorrecta: qué es m[i] ahora que m vale NULL? */
for (i=0; i<filas; i++)
free(m[i]) ;
}

Matrices dinámicas y funciones


El paso de matrices dinámicas a funciones tiene varias formas idiomáticas que conviene que
conozcas. Imagina una función que recibe una matriz de enteros para mostrar su contenido por
pantalla. En principio, la cabecera de la función presentarı́a este aspecto:

void muestra_matriz ( int ** m )

El parámetro indica que es de tipo ((puntero a punteros a enteros)). Una forma alternativa de
decir lo mismo es ésta:

void muestra_matriz ( int * m[] )

Se lee más bien como ((vector de punteros a entero)). Pero ambas expresiones son sinónimas 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ática y que
hemos omitido el número de columnas.
Sigamos con la función:

230 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 4 Estructuras de datos: memoria dinámica

Más eficiencia, menos reservas de memoria


Te hemos enseñado una forma ((estándar)) de pedir memoria para matrices dinámicas. No
es la única. Es más, no es la más utilizada en la práctica. ¿Por qué? Porque obliga a realizar
tantas llamadas a malloc (y después a free) como filas tiene la matriz más uno. Las llamadas
a malloc pueden resultar ineficientes cuando su número es grande. Es posible reservar la
memoria de una matriz dinámica con sólo 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ón de memoria.
18 free(m[0]);
19 free(m);
20

21 return 0;
22 }

La clave está en la sentencia m[i] = m[i-1] + columnas: el contenido de m[i] pasa a ser
la dirección de memoria columnas celdas más a la derecha de la dirección m[i-1]. He aquı́
una representación gráfica de una matriz de 5 × 4:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

m 0

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úmero de filas y columnas explı́citamente para saber
qué 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ón a la Programación con C 231


4.2 Matrices dinámicas 2004/02/10-16:33

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ón que efectúe la liberación de la memoria de
una matriz:
1 void libera_matriz (int ** m, int filas, int columnas)
2 {
3 int i,j;
4

5 for (i=0; i<filas; i++)


6 free(m[i]);
7 free(m);
8 }

Ahora resulta innecesario el paso del número de columnas, pues no se usa en la función:
1 void libera_matriz (int ** m, int filas)
2 {
3 int i,j;
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ón: la asignación 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,j;
4

5 for (i=0; i<filas; i++)


6 free( (*m) [i]);
7 free( *m );
8 *m = NULL ;
9 }

¡Qué horror! ¡Tres asteriscos en la declaración del parámetro m! C no es, precisamente, el colmo
de la elegancia.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 232 Diseña una función que reciba un número de filas y un número de columnas y devuelva
una matriz dinámica de enteros con filas×columnas elementos.
· 233 Diseña un procedimiento que reciba un puntero a una matriz dinámica (sin memo-
ria asignada), un número de filas y un número de columnas y devuelva, mediante el primer
parámetro, una matriz dinámica de enteros con filas×columnas elementos.
.............................................................................................
La gestión de matrices dinámicas considerando por separado sus tres variables (puntero a
memoria, número de filas y número de columnas) resulta poco elegante y da lugar a funciones
con parámetros de difı́cil lectura. En el siguiente apartado aprenderás a usar matrices dinámicas
que agrupan sus tres datos en un tipo registro definido por el usuario.

4.2.2. Definición de un tipo ((matriz dinámica)) y de funciones para su ges-


tión
Presentaremos ahora un ejemplo de aplicación 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á un struct que contendrá una matriz dinámica de float y el número
de filas y columnas.

232 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 4 Estructuras de datos: memoria dinámica

1 struct Matriz {
2 float ** m;
3 int filas, columnas;
4 };

Diseñemos ahora una función que ((cree)) una matriz dado el número de filas y el número 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ón de no pedir memoria si el número de filas o columnas no son


válidos. Para crear una matriz de, por ejemplo, 3 × 4, llamaremos a la función ası́:
1 struct Matriz matriz ;
2 ...
3 matriz = crea_matriz (3, 4);

Hay una implementación 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ón (procedimiento) se llamarı́a ası́:


1 struct Matriz matriz ;
2 ...
3 crea_matriz (3, 4, &matriz );

También nos vendrá 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ón a la Programación con C 233


4.2 Matrices dinámicas 2004/02/10-16:33

5 if (mat->m != NULL) {
6 for (i=0; i<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ámica m, efectuaremos una llamada como ésta:
1 libera_matriz (&m);

Como hemos de leer dos matrices por teclado, diseñemos ahora una función 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ál era el número 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ántos bytes se copiarán
en pila con cada llamada?
· 235 Diseña una nueva versión de muestra_matriz en la que mat se pase por referencia.
¿Cuántos bytes se copiarán en pila con cada llamada?
.............................................................................................
Podemos proceder ya mismo a implementar una función 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

234 Introducción a la Programación con C



c 2003 Andrés Marzal e Isabel Gracia 4 Estructuras de datos: memoria dinámica

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úmero de columnas de la primera
ha de ser igual al número 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ón 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 multiplicables\n");
14 else {
15 printf ("Resultado del producto:\n");
16 muestra_matriz (c);
17 }
18 libera_matriz