Sei sulla pagina 1di 158

Guı́a Práctica de Programación

en C

Septiembre de 2012

Rafael Llobet Azpitarte

EDITORIAL
UNIVERSITAT POLITÈCNICA DE VALÈNCIA
Colección Académica

Para referenciar esta publicación utilice la siguiente cita: LLOBET-AZPITARTE,


R.(2012). Guía práctica de programación en C. Valencia : Editorial Universitat
Politècnica .

Primera edición 2012

© Rafael Llobet- Azpitarte

© todos los nombres comerciales, marcas o signos distintivos de


cualquier clase contenidos en la obra están protegidos por la Ley.

© de la presente edición:
Editorial Universitat Politècnica de València
www.editorial.upv.es

Distribución: pedidos@editorial.upv.es
Tel. 96 387 70 12

Imprime: By print percom sl.

ISBN: 978-84-8363-945-0
Impreso bajo demanda
Ref. editorial: 298

Queda prohibida la reproducción, distribución, comercialización,


transformación, y en general, cualquier otra forma de explotación, por
cualquier procedimiento, de todo o parte de los contenidos de esta obra
sin autorización expresa y por escrito de sus autores.

Impreso en España
Índice general

Índice general I

1 Introducción a la computación 1
1.1 Tratamiento automático de la información . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Codificación de la información: el sistema binario . . . . . . . . . . . . . . . . . . . . . 2
1.3 Concepto de algoritmo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.4 El proceso de la programación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.5 Lenguajes de programación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.6 El lenguaje C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.7 Compiladores e intérpretes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8

2 Elementos básicos de un programa en C 9


2.1 Estructura básica de un programa en C . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.2 Mostrando mensajes con printf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.3 Variables y tipos de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.3.1 Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.3.2 Tipos de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.3.3 Uso de variables: declaración y asignación . . . . . . . . . . . . . . . . . . . . . . . . 13

2.4 Comentarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.5 Uso avanzado de printf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.6 Leyendo datos con scanf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.7 Expresiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.7.1 Expresiones aritméticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.7.2 Expresiones relacionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
I
Índice general

2.7.3 Expresiones lógicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23

2.8 Otros conceptos sobre tipos de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24


2.8.1 El tipo char . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.8.2 Conversión de tipos: casting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26

2.9 Punteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.9.1 Declaración. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.9.2 El operador de dirección & . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.9.3 El operador de indirección * . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.9.4 La constante NULL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30

2.10 Directivas del precompilador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31


2.10.1 Incluir ficheros de cabecera: #include . . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.10.2 Definición de constantes: #define. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32

2.11 Ejercicios resueltos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33


2.12 Ejercicios propuestos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34

3 Estructuras de control 35
3.1 Sentencias de selección . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.1.1 Selección con if-else. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.1.2 Selección con switch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39

3.2 Sentencias de repetición . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42


3.2.1 La sentencia while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
3.2.2 La sentencia do-while. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
3.2.3 La sentencia for. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
3.2.4 Bucles anidados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50

3.3 Algunas técnicas útiles: contadores, acumuladores y banderas . . . . . . . . . . . . . . 51


3.3.1 Contadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
3.3.2 Acumuladores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
3.3.3 Banderas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53

3.4 Ejercicios resueltos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55


3.5 Ejercicios propuestos: condicionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
3.6 Ejercicios propuestos: bucles. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57

4 Funciones 61
4.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.2 Funciones de la librerı́a de C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
II
Índice general

4.3 Creando nuestras propias funciones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63


4.3.1 Cuestiones sintácticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
4.3.2 Control de flujo y transferencia de la información. . . . . . . . . . . . . . . . . . . . . 67
4.3.3 Parámetros y argumentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
4.3.4 Operaciones de Entrada/Salida en las funciones . . . . . . . . . . . . . . . . . . . . . . 70
4.3.5 Prototipos de funciones y ficheros de cabecera . . . . . . . . . . . . . . . . . . . . . . 72

4.4 Variables locales y globales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73


4.5 Paso de argumentos por valor y por referencia . . . . . . . . . . . . . . . . . . . . . . . 76
4.5.1 Paso de argumentos por valor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
4.5.2 Paso de argumentos por referencia. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77

4.6 Ejercicios resueltos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79


4.6.1 Funciones que no devuelven ningún valor . . . . . . . . . . . . . . . . . . . . . . . . . 79
4.6.2 Funciones que devuelven un valor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
4.6.3 Funciones que devuelven un valor del tipo VERDADERO/FALSO . . . . . . . . . . . 81
4.6.4 Funciones que devuelven más de un valor . . . . . . . . . . . . . . . . . . . . . . . . . 82

4.7 Ejercicios propuestos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84

5 Vectores 87
5.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
5.2 Declaración de vectores. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
5.3 Acceso a los elementos de un vector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
5.4 Operaciones con vectores: automatización mediante bucles. . . . . . . . . . . . . . . . 90
5.5 Paso de vectores a una función. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
5.6 Devolución de vectores en una función. . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
5.7 Vectores multidimensionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
5.8 Operaciones con matrices: automatización mediante bucles . . . . . . . . . . . . . . . 99
5.9 Paso de matrices a una función . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
5.10 Relación entre vectores y punteros. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
5.11 Cadenas de caracteres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
5.11.1 Entrada y salida con cadenas de caracteres . . . . . . . . . . . . . . . . . . . . . . . . 106
5.11.2 Funciones de manipulación de cadenas de caracteres . . . . . . . . . . . . . . . . . . 109

5.12 Ejercicios resueltos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110


5.13 Ejercicios propuestos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114

III
Índice general

6 Estructuras 117
6.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
6.2 Declaración de variables de tipos estructurados . . . . . . . . . . . . . . . . . . . . . . . 117
6.2.1 Definición de tipos estructurados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
6.2.2 Declaración de variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118

6.3 Operaciones con estructuras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119


6.3.1 Inicialización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
6.3.2 Acceso a los miembros de una estructura . . . . . . . . . . . . . . . . . . . . . . . . . 119
6.3.3 Asignación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
6.3.4 Otras operaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121

6.4 Estructuras anidadas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122


6.5 Vectores de estructuras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
6.6 Punteros a estructuras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
6.7 Paso de estructuras como parámetros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
6.7.1 Pasar los miembros de forma independiente . . . . . . . . . . . . . . . . . . . . . . . . 125
6.7.2 Pasar una estructura completa por valor . . . . . . . . . . . . . . . . . . . . . . . . . . 125
6.7.3 Pasar una estructura completa por referencia . . . . . . . . . . . . . . . . . . . . . . . 127
6.7.4 Pasar un vector de estructuras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
6.7.5 Devolver una estructura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128

6.8 Ejercicios resueltos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128


6.9 Ejercicios propuestos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131

7 Gestión de ficheros 133


7.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
7.2 Tipos de ficheros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
7.3 Operaciones con ficheros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
7.3.1 Abrir y cerrar ficheros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
7.3.2 Lectura y escritura de ficheros de texto . . . . . . . . . . . . . . . . . . . . . . . . . . 137
7.3.3 Lectura y escritura de ficheros binarios . . . . . . . . . . . . . . . . . . . . . . . . . . 143

7.4 Acceso aleatorio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146


7.5 Ejercicios resueltos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
7.6 Ejercicios propuestos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148

Bibliografı́a 151

IV
Capı́tulo 1

Introducción a la computación

La informática es la ciencia que aborda el tratamiento automático de la información por


medio de ordenadores. Es importante conocer el modo en que un ordenador codifica,
almacena y procesa la información, para poder abordar con soltura la tarea de programa-
ción. El objetivo de este capı́tulo es dar una pequeña introducción a la computación y al
proceso de programación.

1.1 Tratamiento automático de la información


Cualquier sistema de tratamiento de la información, sea o no automático, puede descom-
ponerse, tal y como muestra la figura 1.1, en tres etapas básicas:

Entrada: Recogida de datos.


Proceso: Tratamiento de los datos.
Salida: Presentación de los resultados obtenidos.

Cuando el volumen de datos a tratar es muy elevado o se requiere gran velocidad en el


proceso de los mismos o simplemente la tarea a realizar es lo suficientemente aburrida

Entrada Proceso Salida


S

Figura 1.1: Etapas básicas en el procesamiento de la información


1
Capı́tulo 1. Introducción a la computación

o tediosa para un humano, serı́a deseable realizar este proceso de forma automática. En
este sentido, los ordenadores tienen la capacidad de procesar gran cantidad de datos a una
velocidad de miles de millones de operaciones por segundo.

Para que un ordenador pueda llevar a cabo cualquier tarea, deberá disponer de los elemen-
tos hardware necesarios para la entrada de datos (teclado, ratón, ...), para el procesamiento
de los mismos (procesador o CPU), para la salida de resultados (monitor, impresora, ...) y
para el almacenamiento temporal o permanente de instrucciones y datos (memoria, disco
duro, ...). Además de todos estos elementos, será necesario introducir en el ordenador las
instrucciones necesarias para realizar las operaciones deseadas sobre los datos, esto es,
los programas (software). Los programas son los que hacen que un ordenador sea versátil
y pueda utilizarse para realizar tareas muy diversas. En este libro aprenderemos a crear
dichos programas, pero antes será necesario tener unos conocimientos mı́nimos sobre el
modo en que un ordenador codifica la información.

1.2 Codificación de la información: el sistema binario


Un ordenador funciona por impulsos eléctricos, de modo que únicamente puede diferen-
ciar entre presencia y ausencia de corriente o carga eléctrica, esto es, sólo puede dife-
renciar dos estados. Comúnmente identificamos a estos dos estados mediante los dı́gitos
0 y 1. Es por ello que cualquier información que deba ser procesada por un ordenador,
necesariamente tiene que estar representada mediante una secuencia de unos y ceros, esto
es, debe estar codificada en el sistema binario.

En el sistema decimal que empleamos los humanos, cada dı́gito tiene un peso distinto
según la posición que ocupa. De derecha a izquierda tenemos las unidades, decenas, cen-
tenas, etc, donde el dı́gito que representa las unidades tiene un peso de 100 , el de las
decenas de 101 , el de las centenas de 102 , y ası́ sucesivamente. De modo similar, en el sis-
tema binario, empleado por los ordenadores y otros dispositivos electrónicos, cada dı́gito,
de derecha a izquierda, tiene un peso de 20 , 21 , 22 , etc.

Por ejemplo, el número binario 1101 equivale al decimal 13, ya que 1 ∗ 23 + 1 ∗ 22 + 0 ∗


21 + 1 ∗ 20 = 13.

A los sistemas decimal y binario (por poner algunos ejemplos de sistemas de numeración)
se los denomina también sistema en base 10 y en base 2 respectivamente. En general,
para conocer el valor decimal de cualquier número N expresado en base B aplicaremos la
fórmula: n

N = x0 B 0 + x1 B 1 + ... + xn B n = xi B i
i=0

donde xi son los dı́gitos del número expresado en la base B.

Denominamos bit a un dı́gito binario 0/1 y byte a una agrupación de 8 bits. Un bit úni-
camente nos permite diferenciar entre dos estados posibles. Con una secuencia de 2 bits
2
1.2 Codificación de la información: el sistema binario

pueden formarse 4 combinaciones distintas (00, 01, 10 y 11), con 3 bits 8 combinaciones
(000, 001, 010, 011, 100, 101, 110 y 111). En general, con n bits pueden formarse 2n
combinaciones. Podemos codificar los elementos de un conjunto dado (números enteros,
letras, colores, etc.) mediante distintas secuencias de bits. Por ejemplo, con un byte (8
bits) pueden codificarse 28 = 256 elementos. Con 4 bytes (32 bits) pueden codificarse
más de cuatro mil millones de elementos.

Siguiendo con las unidades de medida, diremos que un kilobyte (1 KB) son 1024 bytes,
un gigabyte (1 GB) son 1024 KB y un terabyte (1 TB) son 1024 GB (esto es, más de mil
millones de bytes).

Ya hemos visto cómo un ordenador puede representar números enteros mediante el siste-
ma binario, pero también debe ser capaz de representar números reales, caracteres, imáge-
nes, sonido y por supuesto instrucciones, por poner sólo algunos ejemplos. Es necesario
disponer de distintos sistemas de codificación para poder representar información de
cualquier tipo.

La codificación es el proceso por el cual representamos sı́mbolos o secuencias de un


alfabeto mediante los sı́mbolos o secuencias de otro alfabeto, es decir, establecemos una
correspondencia biunı́voca entre los sı́mbolos de dos alfabetos distintos. En los ejemplos
anteriores hemos visto cómo codificar números enteros mediante secuencias de 1’s y 0’s
en lo que conocemos como sistema de codificación BCD. Pero, tal y como acabamos de
mencionar, necesitamos otros sistemas para codificar otros objetos de otros conjuntos.

Para codificar los números reales en el sistema binario se utiliza lo que denominamos
representación en coma flotante o formato cientı́fico. Por ejemplo, el número 12.5 (el
cual acabamos de escribir en su notación habitual o coma fija) se expresarı́a en notación
cientı́fica como 0.125E2. La representación binaria de dicho número consiste en la utili-
zación de una cierta cantidad de bits para representar lo que se denomina mantisa (0.125)
y otra cierta cantidad de bits para representar el exponente. Valiéndonos del sistema BCD
utilizado para números enteros, codificarı́amos por un lado el número 125 (mantisa en
formato normalizado) y por otro lado el exponente (2 en este ejemplo).

Otro sistema de codificación es el ASCII (American Standard Code for Information Inter-
change) utilizado para la representación de caracteres alfanuméricos. Para ello se define
una tabla de correspondencia, en la que a cada carácter se le asocia un código binario.
Dicha tabla de correspondencia es un estándar que utilizan todos los sistemas informáti-
cos, lo que permite el intercambio de información entre distintos sistemas. Imaginemos
algo tan habitual como enviar un correo electrónico. El texto en él contenido, se repre-
sentará mediante en una secuencia de 1’s y 0’s, resultado de codificar cada uno de los
caracteres del mensaje mediante la tabla ASCII. Dicha secuencia será sencillamente des-
cifrable por cualquier equipo informático, sin más que aplicar de nuevo la tabla de con-
versión ASCII en sentido inverso. Resulta evidente que sin la existencia de estándares de
codificación, no serı́a posible compartir información entre los distintos ordenadores.

3
Capı́tulo 1. Introducción a la computación

1.3 Concepto de algoritmo


Un algoritmo es una secuencia o conjunto ordenado de operaciones que permiten hallar
la solución de un problema dado en un tiempo finito.

Aunque los algoritmos datan de tiempos babilónicos y los griegos ya diseñaron algorit-
mos, aún famosos hoy en dı́a, como por ejemplo el de Euclides (300 A.C.) para calcular
el máximo común divisor de dos números, fue el matemático persa Al-Khowarizmi (si-
glo IX) el primero que diseñó algoritmos pensando en su eficiencia, en particular, para el
cálculo de raı́ces de ecuaciones. La palabra algoritmo procede de las sucesivas deforma-
ciones sufridas hasta hoy en dı́a del nombre Al-Khowarizmi1 .

Como ejemplo mostraremos el famoso algoritmo de Euclides para obtener el máximo


común divisor de dos números enteros:

1) Sean a y b dos enteros positivos.


2) Comparar los dos números de forma que:
3) Si son iguales, ese es el resultado. Finalizar.

4) Si a es menor que b intercambiar los valores de a y b.


5) Restar el segundo número del primero y almacenar en a el sustraendo y en b el
residuo. Ir al paso 2.

Si aplicamos esta secuencia de reglas a dos números enteros positivos, obtendremos el


máximo común denominador de ambos. Por ejemplo, dados los valores a=5 y b=15, a
continuación se muestra cómo evolucionan las variables a y b a medida que se ejecuta el
algoritmo propuesto, hasta llegar a obtener el valor del máximo común divisor (5):

a b
------------
5 15
15 5
5 10
10 5
5 5

Todo algoritmo debe cumplir las 4 condiciones siguientes:

Finitud: Un algoritmo tiene que acabar siempre tras un número finito de pasos.
1 Hay muchas variantes para el nombre de Al Khowarizmi al usar el alfabeto latino, tales como Al-Khorezmi,

Al-Khwarizmi, Al-Khawarizmi o Al-Khawaritzmi.

4
1.4 El proceso de la programación

Definibilidad: Toda regla debe definir perfectamente la acción a desarrollar sin que
pueda haber ambigüedad alguna de interpretación.
General: Un algoritmo no debe solucionar un problema aislado particular, sino
toda una clase de problemas para los que los datos de entrada y los resultados
finales pertenecen respectivamente a conjuntos especı́ficos.
Eficacia: Se debe pretender que un algoritmo sea eficaz, esto es, que su ejecución
resuelva el problema en el mı́nimo número de operaciones posible.

Se dice que un determinado problema es decidible o computable cuando existe un algo-


ritmo que lo resuelve. Existen, sin embargo, muchas clases de problemas no decidibles.
Estos problemas, por mucho que aumente la capacidad y velocidad de los computadores,
nunca podrán resolverse.

Un ejemplo tı́pico de problema no decidible es el problema de la teselación. Una tesela


(baldosa) es un cuadrado de 1x1, dividido en cuatro partes iguales por sus dos diagonales.
Cada una de estas partes tiene un color. El problema es: dada cualquier superficie finita, de
cualquier tamaño (con dimensiones enteras), ¿puede recubrirse usando teselas de forma
que cada dos teselas adyacentes tengan el mismo color en los lados que se tocan? Este es
un problema indecidible: no se puede demostrar que la superficie pueda recubrirse según
el planteamiento del problema, pero tampoco puede demostrarse lo contrario. No existe
ningún algoritmo que resuelva este problema.

1.4 El proceso de la programación


Un programa es la implementación o escritura de un algoritmo en un lenguaje de progra-
mación especı́fico. Las fases para la correcta elaboración de un programa son las siguien-
tes:

1. Análisis del problema: definir el problema de la manera más precisa posible.


2. Diseño del algoritmo: diseñar un método (algoritmo) que resuelva el problema
planteado.
3. Codificación: escribir (implementar) el algoritmo en un lenguaje de programación.
4. Verificación: comprobación del correcto funcionamiento del programa. Si no fun-
ciona correctamente, dependiendo del error habrá que volver al paso 3, al 2 o inclu-
so al 1.
5. Mantenimiento: desarrollo de mejoras y nuevas funcionalidades.

Estas cinco fases es lo que se denomina el ciclo de vida de un proyecto informático. En


ocasiones el término programación se asocia, de forma incorrecta, únicamente al paso
3, pero para desarrollar correctamente un programa es necesario abordar todas las fases
descritas.
5
Capı́tulo 1. Introducción a la computación

1.5 Lenguajes de programación


Un lenguaje de programación es un conjunto de sı́mbolos y palabras especiales que, junto
con unas reglas sintácticas perfectamente definidas, permiten implementar un algoritmo,
de modo que éste pueda ser ejecutado por un ordenador. En definitiva, un lenguaje de
programación permite construir un programa informático.

En general los lenguajes de programación pueden dividirse en:

Lenguajes de bajo nivel: Son lenguajes cuya sintaxis está más próxima a la máqui-
na (al ordenador) que al humano. El lenguaje de más bajo nivel es el código máqui-
na (cuyas instrucciones son secuencias de unos y ceros), seguido por el lenguaje
ensamblador. Los lenguajes de bajo nivel resultan poco legibles para el humano.
Lenguajes de alto nivel: Son lenguajes cuya sintaxis está más próxima al lenguaje
natural de los humanos y, por tanto, resultan más comprensibles. Existen multitud
de lenguajes de alto nivel como C, C++, Java, Pascal, Basic, PHP, etc. (por citar
sólo unos pocos ejemplos).

A continuación se muestra, a modo de ejemplo, un programa que realiza la suma de


dos números enteros, escrito en tres lenguajes de programación diferentes: C, Pascal y
ensamblador:

Suma de dos números enteros en C:

1 #include <stdio.h>
2

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

6 printf("Introduce 2 números enteros");


7 scanf(" %d %d", &a, &b);
8 c = a + b;
9 printf("La suma es %d\n", c);
10 return 0;
11 }

Suma de dos números enteros en Pascal:

1 program suma;
2

3 var a,b,c:integer;
4 begin
5 writeln(’Introduce 2 números enteros’);
6
1.6 El lenguaje C

6 readln(a,b);
7 c:= a + b;
8 writeln(’La suma es ’,c);
9 end

Suma de dos números enteros en ensamblador:

1 model small
2 .stack
3 .data
4 var1 db ?
5 .code
6 .startup
7 mov ah,01h
8 int 21h
9 sub al,30h
10 mov var1,al
11 mov ah,01h
12 int 21h
13 sub al,30h
14 add al,var1
15 mov dl,al
16 add dl,30h
17 mov ah,02h
18 int 21h
19 ;.exit
20 end

1.6 El lenguaje C
C es un lenguaje estructurado de alto nivel de propósito general. Esto quiere decir que sir-
ve para distintos propósitos: programas cientı́ficos, programas de gestión, comunicación,
manejadores de dispositivos (drivers), sistemas operativos, compiladores, etc.

C dispone de las estructuras tı́picas de los lenguajes de alto nivel pero, a su vez, permite
trabajar a bajo nivel, por lo que algunos autores lo clasifican como lenguaje de medio
nivel.

El lenguaje C fue desarrollado en la década de los 70 por Kenneth Thompson y Dennis


Ritchie en los laboratorios Bell y alcanzó gran popularidad durante la década de los 80
tras la publicación de su definición por parte de Brian Kernighan y el propio Ritchie2 . La
2 Brian W. Kernighan y Dennis M. Ritchie, “The C Programming Languaje”, Prentice-Hall, 1978.

7
Capı́tulo 1. Introducción a la computación

creación del lenguaje C va ligada a otro fenómeno de la informática: la creación del sis-
tema operativo UNIX, desarrollado también en los laboratorios Bell. El núcleo (o kernel)
del sistema operativo Unix está escrito casi en su totalidad en lenguaje C, con excepción
de alguna parte escrita en ensamblador. También el sistema operativo Linux y muchas de
sus aplicaciones están escritas en este mismo lenguaje.

Hoy en dı́a el lenguaje C se sigue utilizando en multitud de programas. Además, muchos


otros lenguajes de programación basan su sintaxis en C, como C++, C#, Java, PHP, Perl,
etc.

1.7 Compiladores e intérpretes


Los ordenadores únicamente entienden el lenguaje máquina. Por tanto, un programa es-
crito en cualquier otro lenguaje debe ser traducido a lenguaje máquina para que pueda
ser ejecutado. Este proceso de traducción se conoce como compilación o interpretación,
dependiendo de cómo se realice.

El proceso de compilación consiste en traducir el programa completamente a lenguaje


máquina antes de ser ejecutado. La interpretación, por contra, consiste en ir traduciendo
las instrucciones una a una o en pequeños grupos, e ir ejecutándolas a medida que se
traducen.

Como puede deducirse, un programa compilado se ejecutará a mayor velocidad que uno
interpretado, ya que en el primer caso la traducción se hace una única vez, de modo que
se almacena en un archivo aparte el programa traducido (denominado código máquina
o ejecutable) y a continuación se ejecuta tantas veces como se desee el código máquina
generado, mientras que en el segundo caso es necesario ir traduciendo las instrucciones
durante la propia ejecución.

El proceso de compilación se realiza mediante un programa especial denominado compi-


lador, mientras que el programa encargado de la interpretación se denomina intérprete.
Un programa dado, dependiendo de el lenguaje en que esté escrito, deberá ser compilado
o interpretado. El lenguaje C requiere del proceso de compilación.

8
Capı́tulo 2

Elementos básicos de un programa


en C

2.1 Estructura básica de un programa en C


Un programa escrito en lenguaje C tiene, en su forma más sencilla, la siguiente estructura
básica:

int main() {
sentencia_1;
sentencia_2;
. . .
sentencia_N;
}

Todo programa en C está compuesto por una serie de instrucciones o sentencias1 , se-
paradas por punto y coma. Estas instrucciones se encuentran dentro de la función main.
En el Capı́tulo 4 se estudiarán en detalle las funciones, de momento es suficiente con
entender que una función es un módulo o bloque de nuestro programa que contiene una
serie de instrucciones y que la función main (función principal) es el punto de inicio de
todo programa en C. Para delimitar el conjunto de instrucciones que contiene una función
(main en nuestro caso) se utilizan los sı́mbolos { y }.

1A lo largo de este libro vamos a utilizar indistintamente los términos instrucción y sentencia.
9
Capı́tulo 2. Elementos básicos de un programa en C

El programa más sencillo que se puede escribir en C es

1 int main() {
2 return 0;
3 }

aunque este programa no harı́a absolutamente nada ya que la única instrucción que con-
tiene (return 0) indica la finalización del mismo.

2.2 Mostrando mensajes con printf


El programa más sencillo, y que además haga algo tangible, es aquel que muestra un
mensaje por pantalla. Por ejemplo:

1 #include <stdio.h>
2

3 int main() {
4 printf("Hola");
5 return 0;
6 }

Ya se ha dicho que la función main es el punto de inicio de cualquier programa en C, por


tanto la primera instrucción que se ejecuta en el programa anterior se encuentra en la lı́nea
4 (más adelante explicaremos la sentencia #include). La sentencia printf muestra un
texto por pantalla, ası́ que el resultado de ejecutar este programa serı́a simplemente

Hola

La sintaxis de printf, en su formato más sencillo, es

1 printf("Texto a mostrar");

En el Apartado 2.5 se estudiarán otras formas de utilizar la sentencia printf.

Además de la función main, vemos que en este programa aparece la instrucción

1 #include <stdio.h>

Sin entrar en detalle, diremos que es necesario incluir esta instrucción para poder utilizar
printf en nuestros programas. En el Apartado 2.10 se explican este tipo de instrucciones
10
2.3 Variables y tipos de datos

con mayor profundidad. De momento únicamente es necesario saber que cualquier pro-
grama que utilice la instrucción printf deberá contener al inicio del mismo la sentencia
#include <stdio.h>.

La instrucción return 0 la pondremos habitualmente como última instrucción de la fun-


ción main. En el Capı́tulo 4 se estudiarán con detalle las funciones y se explicará el uso
de la instrucción return.

2.3 Variables y tipos de datos


Un programa está compuesto por instrucciones y datos. Los datos, al igual que las ins-
trucciones, se almacenan en la memoria del ordenador durante la ejecución del programa.
Cada dato ocupa una posición de memoria, de modo que es posible consultar o modificar
el valor de un dato mediante el acceso a la zona de memoria en la que está almacenado.

2.3.1 Variables
Una variable puede verse como un contenedor donde almacenar un dato determinado. En
realidad una variable representa una posición de memoria en la que se encuentra almace-
nado un dato. Las variables son un mecanismo que ofrecen los lenguajes de programación
para facilitar el acceso a los datos sin necesidad de conocer las posiciones o direcciones
de memoria en que se encuentran almacenados.

Cuando se crea una variable se le asigna un nombre que la identifica. Un nombre de


variable puede constar de uno o más caracteres y debe cumplir las siguientes restricciones:

El primer carácter debe ser una letra o el carácter subrayado ( ), mientras que el
resto pueden ser letras, dı́gitos o el carácter subrayado ( ). Las letras pueden ser
minúsculas o mayúsculas del alfabeto inglés. Por lo tanto, no está permitido el uso
de las letras ’ñ’, ’Ñ’, vocales acentuadas, sı́mbolos especiales, etc.
No pueden existir dos variables con el mismo nombre dentro de una misma función.
El nombre de una variable no puede ser una palabra reservada. Las palabras re-
servadas son identificadores propios del lenguaje. Por ejemplo, int y main son
palabras reservadas del lenguaje C.

Por ejemplo, lo siguiente serı́an nombres válidos de variables: x, y, v1, v2, temperatura,
velocidad_maxima, Vmax.

Los siguientes nombres de variables no son válidos: 1v, velocidad-maxima, Vmáx,


#tag.

11
Capı́tulo 2. Elementos básicos de un programa en C

2.3.2 Tipos de datos


Las variables se clasifican en distintos tipos, según la información que almacenan. Cuan-
do se crea una variable, es necesario especificar a qué tipo de dato pertenece, de este
modo se le asigna memoria suficiente para almacenar valores del tipo especificado. En
C existen cinco tipos de datos básicos: números enteros, números reales con precisión
simple, números reales de doble precisión, caracteres y punteros.

El tipo de dato de una variable determina:

El tamaño (cantidad de bytes) que ocupará la variable en la memoria del ordenador.

El rango de valores que la variable podrá almacenar.


El conjunto de operaciones que se puede realizar sobre dicha variable.

En la Tabla 2.1 se muestran los tipos básicos en C, el tamaño que ocupa cada uno de ellos
en memoria y el rango de valores que pueden almacenar.2

Tabla 2.1: Tipos de datos simples en C

Tipo de dato Nombre del tipo en C Bytes Rango


Carácter char 1 -127 a 128
Entero int 4 -2147483648 a 2147483647
Real float 4 3.4E-38 a 3.4E38
Real (doble precisión) double 8 1.7E-308 a 1.7E308
Puntero (depende del tipo de puntero) 4 -

Como puede observarse en la Tabla 2.1, el tipo char se puede utilizar tanto para represen-
tar caracteres como números enteros pequeños. En la Sección 2.8.1 se explica este tipo de
dato con más detalle. El tipo puntero se emplea para almacenar direcciones de memoria.
En la Sección 2.9 se explican los punteros.

Los tipos char, int, float y double también existen en su versión unsigned (sin signo):
unsigned char, unsigned int, unsigned float y unsigned double. Las varia-
bles de estos tipos únicamente pueden almacenar números positivos, pero con la ventaja
de duplicar el rango de la parte positiva. Por ejemplo, una variable de tipo unsigned char
puede almacenar números enteros entre 0 y 255.

Algunos tipos de datos también admiten los modificadores short y long para disminuir y
aumentar respectivamente el rango de valores (y consecuentemente el espacio de memoria
empleado). Por ejemplo, una variable de tipo short int ocupa 2 bytes (en lugar de los 4
que ocupa el int) y permite almacenar valores entre -32768 y 32768. Una variable de tipo
2 En realidad el tamaño y rango de valores pueden variar en función de la implementación. Los valores dados

son los habituales en un ordenador con arquitectura de 32 bits.


12
2.3 Variables y tipos de datos

unsigned short int ocupa igualmente 2 bytes, pero en este caso puede almacenar
valores entre 0 y 65535.

2.3.3 Uso de variables: declaración y asignación


Antes de poder utilizar una variable en un programa hay que declararla. Para ello se
emplea la siguiente sintaxis:

tipo_de_dato nombre_variable;

Por ejemplo, para declarar una variable de tipo int y nombre x utilizarı́amos la siguiente
sentencia:

1 int x;

Si se desea declarar más de una variable del mismo tipo, puede hacerse separándolas por
comas del siguiente modo:

1 int x, y, z;
2 float a, b;

Las variables pueden declararse:

Al inicio de cada función (por ejemplo de la función principal main). A estas va-
riables se las denomina variables locales.

Fuera de las funciones. A estas variables se las denomina variables globales.

Salvo en casos muy excepcionales, no se recomienda el uso de variables globales. En


la Sección 4.4 se explica con más detalle la diferencia entre ambos tipos de variables.
De momento, únicamente emplearemos variables locales y, por tanto, la variable x del
ejemplo anterior la declararı́amos al inicio de la única función que hasta el momento
hemos visto: main.

1 int main() {
2 int x;
3

4 . . .
5 }

13
Capı́tulo 2. Elementos básicos de un programa en C

Para almacenar una valor en una variable se utiliza la operación de asignación. En el


lenguaje C la asignación se realiza mediante la operación =. Por ejemplo, el siguiente
programa declara una variable real de nombre pi y le asigna el valor 3.14;

1 int main() {
2 float pi;
3 pi = 3.14;
4

5 . . .
6 }

En la operación de asignación, a la izquierda del sı́mbolo = se debe poner siempre el


nombre de la variable que recibe el valor y a la derecha el valor que deseamos almacenar
o una operación cuyo resultado sea un valor.

Por ejemplo la siguiente operación de asignación es incorrecta:

1 3.14 = pi;

Obviamente el valor que recibe la variable debe de ser compatible con el tipo de dato de
la misma. Por ejemplo, no serı́a adecuado declarar una variable de tipo int y tratar de
almacenar en ella un valor real, tal y como muestra el siguiente ejemplo:

1 int main() {
2 int x;
3 x = 2.75;
4

5 . . .
6 }

En este caso en la operación de asignación se producirı́a el truncamiento del número real


de modo que en la variable x se almacenarı́a el valor 2 en lugar de 2.7.

También es posible almacenar en una variable el resultado de una operación (o expre-


sión). El siguiente programa almacena en una variable de nombre area el resultado de
multiplicar 3.14 por 4.

1 int main() {
2 float area;
3 area = 3.14 * 4;
4

5 . . .
6 }

14
2.3 Variables y tipos de datos

En las operaciones también pueden aparecer variables. En este caso la variable que for-
ma parte de la operación se sustituye por su valor. Por ejemplo, el siguiente programa
almacena en la variable perim el resultado de la operación 2 × 3,14 × 3.

1 int main() {
2 float pi, radio, perim;
3 pi = 3.14;
4 radio = 3;
5 perim = 2 * pi * radio;
6

7 . . .
8 }

De forma genérica, la operación de asignación tiene siempre la siguiente estructura:

1 nombre_variable = expresion;

donde expresion puede ser un valor constante (por ejemplo x=2), una variable (por
ejemplo x=y), una operación (por ejemplo x=2*y), etc. En la Sección 2.7 se explica con
más detalle el concepto de expresión. El efecto de la operación de asignación será alma-
cenar en nombre_variable el resultado de expresion.

Es muy importante tener la precaución de que cuando se utilice una variable como parte
de una expresión, dicha variable tenga un valor asignado previamente, de lo contrario el
resultado de la expresión será impredecible. Por ejemplo el siguiente programa, aunque
es sintácticamente correcto, su resultado es impredecible:

1 int main() {
2 float pi, radio, perim;
3 pi = 3.14;
4 perim = 2 * pi * radio;
5 radio = 3;
6

7 . . .
8 }

Debe entenderse que las instrucciones de un programa se ejecutan secuencialmente, co-


menzando con la primera. El error del ejemplo anterior radica en que cuando se ejecuta la
instrucción perim = 2 * pi * radio el valor de la variable radio es desconocido,
ya que todavı́a no se le ha asignado nada. En consecuencia, el resultado almacenado en
perim será impredecible. El hecho de ejecutar posteriormente la instrucción radio=3 ya
no cambia para nada el valor almacenado en perim.

15
Capı́tulo 2. Elementos básicos de un programa en C

El que a una variable no le asignemos nada de forma explı́cita no quiere decir que no
contenga ningún valor, sino que éste es desconocido. En el momento de declarar una
variable, ésta ya contiene un valor, resultado de lo que hubiese previamente en la zona de
memoria que se le asigna, sin embargo, como se ha dicho, este valor es impredecible.

La operación de asignación es destructiva. Esto quiere decir que cuando a una variable
se le asigna un valor, pierde el valor que tuviera anteriormente.

2.4 Comentarios
En un programa es posible (y recomendable) introducir comentarios. Un comentario es
un texto que incluimos en nuestros programas pero que el compilador ignora por com-
pleto. El objetivo de los comentarios es hacer los programas más legibles, ayudando a la
comprensión de los mismos.

En C existen dos modos de añadir comentarios:

Para añadir un comentario de párrafo se encierra el texto a comentar entre los


sı́mbolos /* y */. Un comentario de párrafo puede contener más de una lı́nea. Por
ejemplo:

1 /* Este programa calcula el área de un cı́rculo


2 dado su radio */
3 int main() {
4 float area, radio;
5 . . .
6 }

Para añadir un comentario de lı́nea se utiliza //. El texto que aparece desde //
hasta el final de la lı́nea es ignorado por el compilador. Por ejemplo:

1 int main() {
2 // Declaración de variables
3 float r; // Radio del cı́rculo
4 float a; // Area del cı́rculo
5

6 a = 3.14 * r * r;
7 . . .
8 }

16
2.5 Uso avanzado de printf

2.5 Uso avanzado de printf


En la Sección 2.2 se ha explicado cómo mostrar un texto simple con printf. En ocasio-
nes, junto con el texto, necesitaremos mostrar el valor de alguna variable. Tomemos como
ejemplo el siguiente programa que calcula la suma de dos variables y muestra de forma
incorrecta el resultado:

1 #include <stdio.h>
2

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

6 a = 2;
7 b = 3;
8 c = a + b;
9 printf("La suma de a y b es c");
10 return 0;
11 }

El error del programa anterior radica en que la instrucción printf, tal y como se ha
escrito, mostrará literalmente el texto especificado. Esto es, la ejecución de este programa
mostrará por pantalla el mensaje:

La suma de a y b es c

Si lo que se pretendı́a es que el programa mostrase

La suma de 2 y 3 es 5

entonces la instrucción printf se deberı́a haber escrito del siguiente modo:

1 printf("La suma de %d y %d es %d", a, b, c);

Podemos observar que en la instrucción printf aparece en primer lugar un texto ence-
rrado entre dobles comillas, seguido de una serie de variables separadas por comas. El
texto encerrado entre comillas contiene una serie de códigos especiales %d. Los códigos %
(existen más aparte de %d) indican que esta parte del texto debe ser sustituida por el valor
de alguna expresión (una variable en el caso más sencillo). Concretamente, el código %d
indica que, en la posición en la que aparece, debe mostrarse el valor de una expresión que
de como resultado un valor entero (por ejemplo una variable de tipo int). La expresión
o variable a mostrar será alguna de las que aparezcan a continuación del texto encerra-
17
Capı́tulo 2. Elementos básicos de un programa en C

do entre comillas. La asociación entre los códigos % y las expresiones que aparecen a
continuación se hace por orden de aparición. En consecuencia, en el ejemplo anterior, el
primer código %d se asocia a la variable a, el segundo a la variable b y el tercero a c.
Debe observarse que es necesario que la instrucción printf contenga tantas expresiones
(variables en el ejemplo anterior) como códigos %.

En la Tabla 2.2 se muestra los códigos % (especificadores) que pueden aparecer en una
instrucción printf. En la primera parte de la tabla se muestran los que se usan de forma
más habitual y en los que, de momento, nos centraremos.

Tabla 2.2: Especificadores más habituales.

Especificador Tipo de expresión con que se asocia


%d o %i Entero
%f Real
%c Carácter
%u Entero sin signo
%x o %X Entero (se muestra en notación hexadecimal)
%o Entero (se muestra en notación octal)
%e o %E Real de doble precisión (se muestra en notación exponencial)
%g o %G Real de doble precisión (se muestra en notación cientı́fica)
%s Cadena de caracteres (string)

Además de los especificadores mostrados en la Tabla 2.2, el texto de una instrucción


printf puede contener otras secuencias de caracteres que se interpretan de un modo
especial:

\n: Se sustituye por un salto de lı́nea

\t: Se sustituye por un tabulador

\\: Muestra el carácter \

A continuación se muestra un ejemplo del uso de printf:

1 #include <stdio.h>
2

3 int main() {
4 int entero = 47;
5 float real = 128.75;
6 char car = ’A’;
7

8 printf("Este texto aparece en la primera lı́nea.");


9 printf("Este también.\nAhora en la segunda.\n");
10 printf("\tEsto aparece tabulado.\n");
18
2.6 Leyendo datos con scanf

11 printf("Mostramos el carácter \\.\n");


12 printf("Variable entera: %d\n", entero);
13 printf("Variable real: %f\n", real);
14 printf("Variable de tipo carácter: %c\n", car);
15 printf("Variable entera en hexadecimal: %X\n", entero);
16 printf("Variable entera en octal: %o\n", entero);
17 printf("Variable real en notación exponencial: %E\n",real);
18 printf("Variable real en notación cientı́fica: %G\n", real);
19

20 return 0;
21 }

El ejemplo anterior produce la siguiente salida:

Este texto aparece en la primera lı́nea.Este también.


Ahora en la segunda.
Esto aparece tabulado.
Mostramos el carácter \.
Variable entera: 47
Variable real: 128.750000
Variable de tipo carácter: A
Variable entera en hexadecimal: 2F
Variable entera en octal: 57
Variable real en notación exponencial: 1.287500E+02
Variable real en notación cientı́fica: 128.75

2.6 Leyendo datos con scanf


Habitualmente es necesario que sea el usuario del programa quien introduzca los datos.
La instrucción scanf permite almacenar en variables los datos que el usuario introduce a
través del teclado. La sintaxis de esta instrucción es la siguiente:
scanf("especificadores", lista_de_variables);

donde especificadores contiene una secuencia de códigos % (tantos como datos se


quieran introducir) y lista_de_variables contiene la lista de variables, separadas por
coma, donde se almacenarán los datos. Cada una de estas variables debe ir precedida por
el sı́mbolo & (salvo en algún caso especial que ya se comentará en la Sección 5.11). Los
especificadores o códigos % que usaremos de momento en la instrucción scanf serán %d
(o su equivalente %i), %f y %c para leer variables de tipo entero, real o carácter respecti-
vamente.

Por ejemplo, la siguiente instrucción almacena en la variable x un número que el usuario


debe introducir por teclado:
19
Capı́tulo 2. Elementos básicos de un programa en C

1 scanf(" %d", &x);

En este caso la variable x deberı́a ser de tipo int.

El porqué las variables deben ir precedidas por el sı́mbolo & es algo que se verá en la
Sección 2.9. De momento basta con saber que es necesario hacerlo de este modo.

Cuando se ejecuta una instrucción scanf el programa se detiene en espera de que el


usuario introduzca los datos necesarios (debe pulsarse la tecla enter para finalizar la intro-
ducción). En el momento en que los datos han sido introducidos, éstos se almacenan en las
variables especificadas y el programa continúa. A continuación se muestra un programa
completo que solicita dos números y los suma:

1 #include <stdio.h>
2

3 int main() {
4 float a, b, c;
5

6 printf("Introduzca un número: ");


7 scanf(" %f", &a);
8 printf("Introduzca otro número: ");
9 scanf(" %f", &b);
10 c = a + b;
11 printf("La suma es %f\n", c);
12 return 0;
13 }

Como se puede observar en el programa anterior, es habitual que cada instrucción scanf
vaya precedida de un printf para indicar al usuario de lo que debe hacer.

También es posible leer más de un dato con una única instrucción scanf. Por ejemplo:

1 printf("Introduzca dos números: ");


2 scanf(" %f %f", &a, &b);

En este caso el programa se detendrá en la instrucción scanf hasta que el usuario haya
introducido los dos valores.

20
2.7 Expresiones

2.7 Expresiones
Una expresión es una combinación de constantes, variables, sı́mbolos de operación, parénte-
sis y funciones. Por ejemplo, lo siguiente son expresiones válidas en C:

1 a-(b+3)*c
2 2*3.1416*r
3 sin(x)/2

En la última expresión del ejemplo aparece una llamada a la función seno (sin). Las fun-
ciones se estudiarán en el Capı́tulo 4.

Las expresiones se evalúan a un valor determinado, esto es, el resultado de una expre-
sión siempre es un valor (entero o real). Las expresiones en C pueden clasificarse en tres
categorı́as:

Aritméticas
Relacionales
Lógicas

En los siguientes apartados se explica cada una de ellas.

2.7.1 Expresiones aritméticas


Las expresiones aritméticas son aquellas que utilizan los operadores +, -, *, / y %. El
resultado de una expresión aritmética es un valor entero o real.

El operador * representa la multiplicación. El operador / realiza la división, si bien debe


llevarse especial cuidado con esta operación ya que, dependiendo de los operandos, rea-
lizará la división entera o la real. Si al menos uno de los dos operandos es un valor real,
entonces se hará la división real, pero si ambos operandos son enteros se realizará la di-
visión entera. Por ejemplo, el resultado de la expresión 5 / 2 es 2 (división entera) y no
2.5 (división real) ya que tanto 5 como 2 son números enteros. Sin embargo, el resultado
de 5.0 / 2 es 2.5 ya que, en este caso, alguno de los operandos (el primero de ellos en
este ejemplo) es un número real.

El operador % realiza la operación módulo o resto de la división entera. Por ejemplo


11 % 3 = 2 ya que la división entera de 11 y 3 da cociente 3 y resto 2. En general, el
resultado de a % b será un número comprendido entre 0 y b-1, ya que el resto debe de ser
necesariamente menor que el divisor. La operación módulo únicamente puede realizarse
entre números enteros y puede ser útil para diversas situaciones. Por ejemplo, para obtener
la última cifra de un número n basta con hacer n % 10 (por ejemplo, 127 % 10 = 7). Si
21
Capı́tulo 2. Elementos básicos de un programa en C

se desean las dos últimas cifras habrá que hacer n % 100 (127 % 100 = 27). Para saber
si un número n es par o impar habrá que calcular n % 2. Si el resultado de esta operación
es 0 quiere decir que n es par, mientras que si el resultado es 1 significará que n es impar.

Para evaluar las expresiones aritméticas existen unas reglas de prioridad y asociatividad:

Las operaciones entre paréntesis se evalúan primero.


Los operadores *, / y % se evalúan antes (tienen mayor prioridad) que + y -.
Los operadores de igual prioridad se evalúan de izquierda a derecha.

Por ejemplo, el resultado de 3 + 2 * 4 es 11 (se realiza en primer lugar la multiplica-


ción) mientras que (3 + 2) * 4 da 20 (se realiza en primer lugar la suma).

Operadores de incremento y decremento

Los operadores ++ y -- sirven, respectivamente, para incrementar y decrementar en una


unidad el valor de una variable. Por ejemplo, n++ incrementa en uno el valor de la varia-
ble n, mientras que n-- lo decrementa. Como puede observarse, se trata de operadores
unarios (únicamente tienen un operando).

La operación n++ es equivalente a n = n + 1 (se calcula n + 1 y el resultado se


almacena en la propia variable n, lo que provoca que el valor de n acabe incrementándose
en 1). De forma análoga, n-- es equivalente a n = n - 1.

También es posible escribir el operador antes del operando (++n y --n). Esto no introduce
ninguna diferencia cuando la operación se hace de forma aislada, aunque es importante
tenerlo en cuenta cuando esta operación se combina con otras dentro de la misma ex-
presión, ya que modifica el orden en el que se realiza el incremento o decremento. Por
ejemplo x / y++ calcula primero la división x / y y a continuación incrementa la va-
riable y, mientras que x / ++y incrementa en primer lugar el valor de y y a continuación
realiza la división (con el valor de y ya incrementado).

Operadores reducidos

Los operadores reducidos más habituales son +=, -=, *= y /=. Estos operadores permiten
expresar de forma más compacta algunas operaciones. Por ejemplo, x += 2 equivale a
la operación x = x + 2 (esto es, incrementa el valor de la variable x en dos unidades).
x *= 5 equivale a la operación x = x * 5.

A continuación se muestra un ejemplo con el uso de las operaciones aritméticas vistas:

22
2.7 Expresiones

1 #include <stdio.h>
2

3 int main() {
4 int x, y, z;
5

6 x = 6;
7 x--; // x vale 5
8 y = x / 2; // y vale 2 (se realiza la división entera)
9 z = y++; // z vale 2 e y vale 3 (primero se realiza
10 // la asignación y luego el incremento)
11 x += 3; // x vale 8
12 x *= 2; // x vale 16
13 z = x % 7; // z vale 2 (el resto de dividir 16 entre 7)
14

15 return 0;
16 }

2.7.2 Expresiones relacionales


Las expresiones relacionales son aquellas que comparan valores. Para ello se utilizan los
operadores <, <=, >, >=, == y != (que se corresponden, respectivamente, con las opera-
ciones menor, menor o igual, mayor, mayor o igual, igual y distinto). El resultado de una
expresión relacional es 1 cuando el resultado de la comparación es cierto y 0 cuando el
resultado es falso. Por ejemplo, dado x = 5, el resultado de la operación x >= 5 es 1
(cierto) y el de x != 5 es 0 (falso).

No debe confundirse la operación de comparación == con la operación de asignación =


(explicada en la Sección 2.3.3). Por ejemplo, la expresión x==5 comprueba si la variable
x vale 5. En caso afirmativo el resultado de esta expresión será 1 y de lo contrario dará 0.
Por contra, la operación x=5 almacena el valor 5 en la variable x.

2.7.3 Expresiones lógicas


Las expresiones lógicas son aquellas que utilizan los operadores lógicos &&, ||, y !, los
cuales se corresponden, respectivamente, con las operaciones ’Y’, ’O’ y ’NO’ (AND, OR
y NOT). Las expresiones lógicas permiten combinar otras expresiones cuyo resultado es
verdadero/falso (1/0). A su vez, el resultado de una expresión lógica es 1 (verdadero) o 0
(falso).

Por ejemplo, dadas las variables a=5 y b=2:

a != 0 && b >= a (a distinto de cero y b mayor o igual que a) se evalúa a 0


(falso).
23
Capı́tulo 2. Elementos básicos de un programa en C

a == 4 || a+b < 8 (a igual a cuatro o a+b menor que ocho) se evalúa a 1 (cierto).

!(b > a) (no b mayor que a) se evalúa a 1 (cierto).

Los siguientes puntos resumen el uso de los operadores lógicos:

exp1 && exp2 se evalúa a cierto cuando tanto exp1 como exp2 son ciertas. En
cualquier otro caso se evalúa a falso.

exp1 || exp2 se evalúa a cierto cuando alguna de las expresiones exp1 o exp2
son ciertas. Se evalúa a falso cuando tanto exp1 como exp2 son falsas.
!exp1 se evalúa a cierto cuando exp1 es falso y viceversa.

Estos tres puntos se resumen en la Tabla 2.3 denominada taba de verdad.

Tabla 2.3: Tabla de verdad de los operadores lógicos.

exp1 exp2 exp1 && exp2 exp1 || exp2 !exp1


0 0 0 0 1
0 1 0 1 1
1 0 0 1 0
1 1 1 1 0

2.8 Otros conceptos sobre tipos de datos

2.8.1 El tipo char


Como se ha visto en la Sección 2.3.2, el lenguaje C utiliza el tipo char para almacenar
caracteres. En realidad el ordenador trabaja únicamente con números, por lo que cada
carácter se representa mediante un número. Esto es, el tipo char realmente almacena
números, los cuales pueden ser interpretados como caracteres.

La interpretación de qué carácter hay almacenado en una variable de tipo char se realiza
mediante una tabla de conversión. La tabla más conocida (por ser la primera que apareció)
es el estándar ASCII (American Standard Code for Information Interchange). En la Ta-
bla 2.4 se muestra la tabla ASCII. En esta tabla, los códigos del 0 al 31 en realidad no son
caracteres imprimibles sino que representan códigos de control (por ejemplo, el código
13 representa el salto de lı́nea o Carriage Return, mientras que el código 27 representa
la tecla Escape). Entre el 65 y el 90 se codifican las letras mayúsculas, mientras que las
minúsculas ocupan los códigos del 97 al 122. Los caracteres numéricos (dı́gitos del 0 al
9) aparecen entre los códigos 48 al 57.

24
2.8 Otros conceptos sobre tipos de datos

Tabla 2.4: Tabla ASCII

0 NUL 16 DLE 32 SP 48 0 64 @ 80 P 96 ‘ 112 p


1 SOH 17 DC1 33 ! 49 1 65 A 81 Q 97 a 113 q
2 STX 18 DC2 34 ” 50 2 66 B 82 R 98 b 114 r
3 ETX 19 DC3 35 # 51 3 67 C 83 S 99 c 115 s
4 EOT 20 DC4 36 $ 52 4 68 D 84 T 100 d 116 t
5 ENQ 21 NAK 37 % 53 5 69 E 85 U 101 e 117 u
6 ACK 22 SYN 38 & 54 6 70 F 86 V 102 f 118 v
7 BEL 23 ETB 39 ’ 55 7 71 G 87 W 103 g 119 w
8 BS 24 CAN 40 ( 56 8 72 H 88 X 104 h 120 x
9 HT 25 EM 41 ) 57 9 73 I 89 Y 105 i 121 y
10 LF 26 SUB 42 * 58 : 74 J 90 Z 106 j 122 z
11 VT 27 ESC 43 + 59 ; 75 K 91 [ 107 k 123 {
12 FF 28 FS 44 , 60 < 76 L 92 \ 108 l 124 |
13 CR 29 GS 45 - 61 = 77 M 93 ] 109 m 125 }
14 SO 30 RS 46 . 62 > 78 N 94 ˆ 110 n 126 ∼
15 SI 31 US 47 / 63 ? 79 O 95 _ 111 o 127 DEL

Debe aclararse que la tabla ASCII se definió para 7 bits, lo que permite representar 128
caracteres. Esto es insuficiente para muchas lenguas que utilizan caracteres que no están
representados en esta tabla (vocales acentuadas, Ñ, Ç, etc.). Por ello se definieron otros
estándares de 8 bits que permitı́an extender el número de caracteres a 256 (por ejemplo el
ISO-8859-1).

En C los caracteres deben encerrarse entre comillas simples. Por ejemplo, para almacenar
el carácter A en una variable lo haremos del siguiente modo:

1 char c;
2 c = ’A’;

Dado que las variables de tipo char realmente almacenan números, es posible realizar
operaciones aritméticas con este tipo de variables, tal y como se muestra en el siguiente
programa:

1 #include <stdio.h>
2

3 int main() {
4 char c;
5

6 c = ’A’; // Almacena en c el número 65 (código


7 // ascii de la letra A)
8 printf(" %d\n", c); // Imprime el número 65
9 printf(" %c\n", c); // Imprime la letra A
10 c += 5; // Almacena en c el número 70
11 printf(" %d\n", c); // Imprime el número 70
12 printf(" %c\n", c); // Imprime una F (cuyo código ascii es 70)
25
Capı́tulo 2. Elementos básicos de un programa en C

13 c = 68; // Almacena en c el número 68


14 c++; // c vale 69
15 printf(" %d\n", c); // Imprime el número 69
16 printf(" %c\n", c); // Imprime la letra E
17 c = ’2’; // Almacena en c el número 50 (código
18 // ascii del carácter ’2’)
19 printf(" %d\n", c); // Imprime el número 50
20 printf(" %c\n", c); // Imprime el carácter ’2’
21

22 return 0;
23 }

2.8.2 Conversión de tipos: casting


En ocasiones se utilizan expresiones en las que no todos los datos son del mismo tipo.
Imaginemos el siguiente código:

1 float f;
2 f = 5;

La instrucción f = 5 asigna un valor entero a una variable de tipo real, produciéndose


una conversión de entero a real. En este caso se produce una conversión sin pérdida de
precisión. Otras conversiones, sin embargo, pueden conllevar pérdida de precisión, tal y
como se muestra en el siguiente código:

1 float radio = 2.5;


2 int area;
3 area = 3.1416 * r * r;

En este caso el resultado de la operación 3.1416 * r * r se convierte a entero pa-


ra poder almacenarlo en la variable area, de tipo int, con la consiguiente pérdida de
precisión.

Si ordenamos los tipos de datos de menor a mayor capacidad (char, int, float, double),
cualquier conversión de un tipo de menor capacidad a otro de mayor puede realizarse sin
pérdida de precisión, mientras que una conversión en el otro sentido (de mayor a menor
capacidad) puede conllevar pérdida de información. El compilador realiza de forma au-
tomática estos cambios de tipo, si bien en los casos que impliquen pérdida de información
puede generar un warning para advertir al programador del posible error.

En ocasiones es el propio programador el que desea hacer esta conversión de forma


explı́cita, bien para dejar claro que desea hacer la conversión y evitar los posibles war-
nings dados por el compilador, bien para forzar un cambio de tipo en aquellos casos en los
26
2.8 Otros conceptos sobre tipos de datos

que el compilador no la harı́a de forma automática. Esta conversión explı́cita se conoce


por el nombre de casting. Para realizar el cambio de tipo de una expresión debe escribir-
se el nuevo tipo entre paréntesis a la izquierda de la expresión, tal y como se muestra a
continuación::

1 float radio = 2.5;


2 int area;
3 area = (int)(3.1416 * r * r);

Mediante esta conversión explı́cita el programador deja claro que es consciente del cam-
bio de tipo que se va a producir, con la consecuente pérdida de precisión.

También hay ocasiones en las que se desea forzar un cambio de tipo ya que el compilador
no lo hace de manera automática. Tomemos el siguiente ejemplo:

1 int suma=5, n=2;


2 float media;
3 media = suma/n;

En este caso el valor almacenado en media será 2.0 y no 2.5. Esto es debido (tal y como
se explicó en la Sección 2.7.1) a que la operación suma/n realiza la división entera, ya
que ambos operandos (suma y n) son enteros. El hecho de que el resultado se almacene
posteriormente en una variable de tipo float no cambia para nada la situación ya que en
este caso la pérdida de precisión se produce en la propia operación de división y no en la
posterior asignación. Para solucionar este problema deberı́a forzarse el cambio de tipo de
al menos uno de los operandos para que se realice la división real, tal y como se muestra
a continuación:

1 int suma=5, n=2;


2 float media;
3 media = (float)suma/n;

En este caso la variable suma se cambia temporalmente a tipo float, con lo que se
realiza la división real. Debe quedar claro que este cambio de tipo afecta únicamente
a la operación en la que aparece el casting, en el resto del programa la variable suma
seguirá siendo de tipo entero.

27
Capı́tulo 2. Elementos básicos de un programa en C

2.9 Punteros
La memoria del ordenador puede verse como una serie de celdas en las que se almacena la
información. Cada una de estas celdas tiene asociada una dirección de memoria. Cuando
declaramos una variable (por ejemplo int x), a ésta se le asigna una celda de memoria
con una dirección conocida. Hasta ahora no nos habı́amos preocupado por la dirección
de memoria en la que se ubican nuestras variables, nos bastaba con conocer su nombre
para poder acceder al dato almacenado en las mismas. Este es el momento de empezar a
preguntarnos en qué dirección se encuentran las variables de nuestros programas.

Una variable de tipo puntero es una variable que almacena direcciones de memoria. Nor-
malmente se utilizan para almacenar la dirección de memoria en la que se encuentra
alguna otra variable. Cuando una variable de tipo puntero (por ejemplo p) contiene la
dirección de memoria de alguna otra variable (por ejemplo x) decimos que el puntero p
apunta a la variable x. Como veremos más adelante, una variable de tipo puntero de-
be conocer el tipo de dato de la variable a la cual apunta (o, en general, el tipo de dato
almacenado en la dirección de memoria que contiene).

2.9.1 Declaración
Para declarar una variable de tipo puntero se emplea la siguiente sintaxis:

tipo_del_dato_apuntado * nombre_del_puntero;

Por ejemplo

1 int * p;

declara una variable puntero de nombre p que debe utilizarse para almacenar direcciones
de memoria en las que se encuentren datos de tipo int.

2.9.2 El operador de dirección &


El operador de dirección & (también llamado de referencia) obtiene la dirección de me-
moria de una variable. Por ejemplo, dada una variable x, la operación &x obtiene la di-
rección de memoria donde se encuentra x, tal y como se muestra en el siguiente ejemplo:

1 #include <stdio.h>
2

3 int main() {
4 int x = 3;
5 printf("La variable x contiene el valor %f y se encuentra en
la dirección %d\n", x, &x);
28
2.9 Punteros

6 return 0;
7 }

Si quisiéramos almacenar la dirección de x en otra variable, entonces esta otra variable


deberı́a ser de tipo puntero, tal y como se muestra en el siguiente ejemplo:

1 #include <stdio.h>
2

3 int main() {
4 int x = 3;
5 int * p; // Declaramos una variable de tipo puntero a
entero
6 p = &x; // Guardamos en p la dirección de memoria de la
variable x
7 printf("La variable x contiene el valor %f y se encuentra en
la dirección %d\n", x, p);
8 return 0;
9 }

2.9.3 El operador de indirección *


El operador de indirección * (también llamado de deferencia) se aplica sobre punteros y
devuelve el valor de la variable apuntada por el puntero. A continuación se muestra un
ejemplo:

1 #include <stdio.h>
2

3 int main() {
4 int x = 3, y;
5 int * p; // Declaramos una variable de tipo puntero a
entero
6 p = &x; // Guardamos en p la dirección de x
7 y = *p; // Almacenamos en y el valor de la variable
8 // apuntada por p, esto es, el valor de x
9 *p = 2; // Ahora x vale 2
10 p = &y; // p contiene la dirección de y (p apunta a y)
11 *p = 5 * 3; // Ahora y vale 15
12 return 0;
13 }

En general, si un puntero p contiene la dirección de cierta variable x (p=&x), entonces la


expresión *p equivale a x. Por ejemplo, la operación *p=2 es equivalente a x=2.

29
Capı́tulo 2. Elementos básicos de un programa en C

Como puede verse, el operador * tiene tres usos distintos en C:

Para declarar variables de tipo puntero. Por ejemplo, int * p;


Como operador de indirección. Por ejemplo, *p = 2;

Como operador de multiplicación. Por ejemplo, 5 * 3;

Como puede observarse en el ejemplo anterior, la instrucción *p = 5 * 3 utiliza el *


con dos significados distintos.

El porqué nos interesa almacenar la dirección de una variable en un puntero y luego ac-
ceder a dicha variable a través de su puntero en lugar de utilizar su propio nombre es algo
que a estas alturas es difı́cil de explicar, aunque debe decirse que es de gran importan-
cia entender el manejo de punteros para poder entender en profundidad muchos aspectos
de la programación. De forma muy simplificada diremos que en ocasiones puede haber
alguna parte de un programa en la que lo único que se conozca de algunas variables sea
las direcciones de memoria que ocupan pero no sus nombres (los cuales pueden inclu-
so ni existir). En estos casos el único modo de acceder a dichas variables es mediante
el manejo de punteros. De momento basta con entender la sintaxis y manejo de punte-
ros, en capı́tulos posteriores, en la Sección 4.5.2, se verá una situación en la que resultan
imprescindibles.

2.9.4 La constante NULL


Una variable de tipo puntero, además de direcciones de memoria, también puede almace-
nar la constante NULL. Este valor sirve para indicar que el puntero no contiene ninguna
dirección de memoria válida. Por ejemplo:

1 int * p;
2 p = NULL; // p no contiene ninguna dirección de memoria

Para poder utilizar la constante NULL debe incluirse el fichero stdlib.h mediante la
instrucción

1 #include <stdlib.h>

30
2.10 Directivas del precompilador

2.10 Directivas del precompilador


El precompilador es un programa que analiza y modifica el fichero fuente antes de la
compilación real, en función de ciertas directivas de compilación que podemos incluir en
nuestros programas. Todas las directivas de compilación comienzan por el carácter # (por
ejemplo, #include) y, a diferencia de las sentencias de C, no llevan punto y coma al
final. El precompilador, además, elimina todos los comentarios del código fuente, ya que
éstos no forman parte del lenguaje y por tanto no serı́an entendidos por el compilador. A
continuación se explican las directivas #include y #define que utilizaremos de forma
habitual en nuestros programas:

2.10.1 Incluir ficheros de cabecera: #include


La directiva #include se utiliza para incluir en nuestro fichero fuente el contenido de
otros ficheros. Habitualmente los ficheros que se incluyen son los denominados ficheros
de cabecera (header files). La sintaxis empleada es:

#include <fichero>

#include "fichero"

Por ejemplo, la siguiente directiva incluye el fichero de cabecera stdio.h

1 #include <stdio.h>

El fichero stdio.h (standard input output header) contiene el código necesario para que
se compilen correctamente todas las funciones relacionadas con la entrada/salida (inpu-
t/output) de nuestro programa, como por ejemplo printf y scanf. En la Sección 4.3.5
se explica con más detalle los ficheros de cabecera.

La diferencia entre utilizar los corchetes angulados (<...>) y las dobles comillas ("...")
radica en que, en el primer caso, el fichero incluido será buscado en los directorios que
el compilador tenga configurados a tal efecto, mientras que en el segundo caso el fichero
incluido será buscado en el mismo directorio en el que se encuentra el fichero fuente.

31
Capı́tulo 2. Elementos básicos de un programa en C

2.10.2 Definición de constantes: #define


La directiva #define se emplea para definir constantes (en realidad también para definir
macros, aunque nosotros no la emplearemos con esta finalidad). La sintaxis empleada es:

#define nombre_constante valor

Por ejemplo, la directiva

1 #define PI 3.1416

define la constante PI con el valor especificado. Una vez definida una constante, ésta
puede utilizarse a lo largo de todo el programa:

1 #include <stdio.h>
2 #define PI 3.1416
3

4 int main() {
5 float radio, perim;
6

7 printf("Introduce valor del radio: ");


8 scanf(" %f", &radio);
9 perim = 2 * PI * radio;
10 printf("El perı́metro de la circunferencia es %f\n", perim);
11 return 0;
12 }

En este caso el precompilador buscará el texto PI como parte de alguna expresión y lo


sustituirá por 3.1416, de modo que cuando se inicie el proceso de compilación la lı́nea

1 perim = 2 * PI * radio;

habrá sido sustituida por

1 perim = 2 * 3.1416 * radio;

32
2.11 Ejercicios resueltos

2.11 Ejercicios resueltos


1. Escribir un programa que solicite la base y la altura de un rectángulo y muestre por
pantalla el área y el perı́metro.
SOLUCIÓN:

1 #include <stdio.h>
2

3 int main() {
4 float base, alt, area, perim;
5

6 // Pedir datos de entrada


7 printf("Introduce la base: ");
8 scanf(" %f", &base);
9 printf("Introduce la altura: ");
10 scanf(" %f", &alt);
11

12 // Calcular resultados
13 area = base * alt;
14 perim = 2*base + 2*alt;
15

16 // Mostrar resultados
17 printf("Area = %f\n", area);
18 printf("Perı́metro = %f\n", perim);
19

20 return 0;
21 }

2. Indicar qué mostrarı́a por pantalla el siguiente programa:

1 #include <stdio.h>
2

3 int main() {
4 int a, b=5, res;
5 char c=’A’;
6

7 a = 2;
8 printf(" %d\n", b/a);
9 a *= 2;
10 b--;
11 printf(" %d\n", a==b);
12 c += a;
13 printf(" %c\n", c);
14 res = ( c==’A’ || ( a > 0 && b < 5 ) );
15 printf(" %d\n", res);
16

33
Capı́tulo 2. Elementos básicos de un programa en C

17 return 0;
18 }

SOLUCIÓN:

2
1
E
1

2.12 Ejercicios propuestos


1. Se pretende calcular el importe del combustible que consume un vehı́culo durante
un determinado trayecto. Para ello se pide escribir un programa que solicite como
datos de entrada: el consumo medio del vehı́culo (litros/100 km.), los kilómetros
del trayecto y el precio del litro de combustible. Con esos datos, el programa de-
berá calcular y mostrar: el total de litros consumidos y el coste total.
2. Indicar qué mostrarı́a por pantalla el siguiente programa:

1 #include <stdio.h>
2

3 int main() {
4 int a, b, c;
5 int * p1, * p2;
6

7 a = 2;
8 p1 = &b;
9 p2 = &c;
10 *p1 = a * 2;
11 *p2 = *p1 + 3;
12 printf("a= %d b= %d c= %d\n", a, b, c);
13 p2 = &a;
14 *p1 += *p2;
15 printf("a= %d b= %d c= %d\n", a, b, c);
16 c = *p1 + *p2;
17 printf("a= %d b= %d c= %d\n", a, b, c);
18 return 0;
19 }

34
Capı́tulo 3

Estructuras de control

En los programas que hemos realizado hasta ahora, cada una de las instrucciones se eje-
cuta en modo secuencial, una tras otra y una única vez. Sin embargo, es habitual que
los programas necesiten ejecutar, en función de cierta condición, un grupo de instruccio-
nes u otro (ejecución condicional), o que, por ejemplo, requieran ejecutar un bloque de
instrucciones más de una vez (bucle). Estas situaciones se resuelven mediante lo que se
denomina sentencias de selección y sentencias de repetición.

3.1 Sentencias de selección


Las sentencias de selección permiten ejecutar unas instrucciones u otras, en función de
cierta condición. En este sentido, el lenguaje C dispone de las instrucciones if-else y
switch.

3.1.1 Selección con if-else


La instrucción if-else permite ejecutar un bloque de instrucciones u otro, dependiendo
de que la evaluación de una expresión lógica resulte ser verdadera o falsa. La sintaxis
general de esta instrucción es la siguiente:

if( expresion ) {
instruccion_1_1;
instruccion_1_2;
. . .
instruccion_1_N;
}
else {
instruccion_2_1;
35
Capı́tulo 3. Estructuras de control

instruccion_2_2;
. . .
instruccion_2_N;
}

Si la expresión de la instrucción if se evalúa a 1 (cierto), se ejecutarán las instrucciones


1_1 a 1_N, en caso contrario se ejecutarán las instrucciones 2_1 a 2_N.

Por ejemplo, el siguiente programa indica si cierta nota introducida por teclado corres-
ponde a un aprobado o a un suspenso:

1 #include <stdio.h>
2

3 int main() {
4 float nota;
5

6 printf("Introduce el valor de la nota: ");


7 scanf(" %f", &nota);
8 if( nota >= 5 ) {
9 printf("APROBADO\n");
10 }
11 else {
12 printf("SUSPENDIDO\n");
13 }
14 return 0;
15 }

Cuando alguno de los bloques if o else consta de una única instrucción, entonces las
llaves son opcionales.

if( expresion )
instruccion_1;
else
instruccion_2;

Por lo tanto la instrucción if-else del ejemplo anterior también podrı́a haberse escrito
como:

1 if( nota >= 5 )


2 printf("APROBADO\n");
3 else
4 printf("SUSPENDIDO\n");

Por otro lado, debe resaltarse que el bloque else es opcional. En el siguiente ejemplo se
utiliza una instrucción if sin el bloque else:
36
3.1 Sentencias de selección

1 #include <stdio.h>
2

3 int main() {
4 float precio;
5 char aplicar_descuento;
6

7 printf("Introduce el precio del artı́culo: ");


8 scanf(" %f", &precio);
9 printf("¿Desea aplicar descuento? (s/n) ");
10 scanf(" %c", &aplicar_descuento);
11

12 if( aplicar_descuento == ’s’ )


13 precio = precio * 0.9; // Aplico un descuento del 10 %
14

15 printf("Total a pagar: %.2f\n", precio);


16

17 return 0;
18 }

Las instrucciones if-else pueden anidarse, esto es, tanto dentro del bloque if como del
else pueden aparecer otras instrucciones if-else. En el siguiente ejemplo se muestra
un programa que solicita tres números y muestra el mayor de ellos mediante el uso de
if-else anidados:

1 #include <stdio.h>
2

3 int main() {
4 float a, b, c, max;
5

6 printf("Introduce tres números: ");


7 scanf(" %f %f %f", &a, &b, &c);
8

9 if ( a > b ) { // El máximo será a o c


10 if( a > c )
11 max = a;
12 else
13 max = c;
14 }
15 else { // El máximo será b o c
16 if( b > c )
17 max = b;
18 else
19 max = c;
20 }
21 printf("El máximo es %f\n", max);

37
Capı́tulo 3. Estructuras de control

22

23 return 0;
24 }

Otro modo de resolver el problema anterior podrı́a ser el siguiente:

1 . . .
2

3 if ( a>b && a>c)


4 max = a;
5 else {
6 if( b > c )
7 max = b;
8 else
9 max = c;
10 }
11 printf("El máximo es %f\n", max);

En ocasiones es necesario anidar un número elevado de instrucciones if-else con el fin


de seleccionar una de entre varias acciones. La sintaxis en este caso no difiere para nada
de lo expuesto anteriormente, sin embargo, por cuestiones de legibilidad, el código suele
escribirse de modo algo distinto, tal y como se muestra a continuación:

if( expresion_1 ) {
instrucciones;
}
else if (expresion_2) {
instrucciones;
}
else if (expresion_3) {
instrucciones;
}
. . .

Debe observarse que el código que acabamos de escribir coincide con el que se muestra a
continuación, aunque el primero resulta más legible:

if( expresion_1 ) {
instrucciones;
}
else {
if (expresion_2) {
instrucciones;
}
else {
38
3.1 Sentencias de selección

if (expresion_3) {
instrucciones;
}
. . .
}
}

El siguiente ejemplo muestra la calificación obtenida (suspenso, aprobado, notable o so-


bresaliente) en función de la nota numérica, mediante la concatenación de varias instruc-
ciones if-else:

1 #include <stdio.h>
2

3 int main() {
4 float nota;
5

6 printf("Introduce el valor de la nota: ");


7 scanf(" %f", &nota);
8

9 if( nota >= 9 )


10 printf("SOBRESALIENTE\n");
11 else if( nota >= 7 )
12 printf("NOTABLE\n");
13 else if( nota >= 5 )
14 printf("APROBADO\n");
15 else
16 printf("SUSPENDIDO\n");
17

18 return 0;
19 }

3.1.2 Selección con switch


Tal y como se ha visto en el apartado anterior, dados dos grupos de instrucciones, la
instrucción if-else permite seleccionar uno de ellos. Si se desea seleccionar entre más
de dos opciones, se ha visto que es necesario utilizar una combinación de instrucciones
if-else.

Otra alternativa es es utilizar la instrucción switch. Antes de pasar a ver la sintaxis de


esta instrucción, debe quedar claro que cualquier algoritmo que se implemente utilizan-
do switch puede implementarse igualmente mediante una combinación de instrucciones
if-else. La instrucción switch simplemente proporciona otra manera de escribir cier-
tas partes de un programa, lo que en ocasiones puede ofrecer mayor legibilidad a nuestro
código.

39
Capı́tulo 3. Estructuras de control

La sintaxis de la instrucción switch es la siguiente:

switch( expresion ) {
case valor_1: instrucciones;
case valor_2: instrucciones;
. . .
case valor_N: instrucciones;
default: instrucciones;
}

La expresión de la sentencia switch debe evaluarse a un entero o a un carácter (en la


mayorı́a de los casos esta expresión será simplemente una variable de tipo int o char).
Si el resultado de dicha expresión coincide con el valor especificado en alguna de las sen-
tencias case, entonces se ejecutarán todas las instrucciones que aparecen a continuación
de dicho case, hasta que se encuentre una instrucción break. Si el valor no coincide
con ningún case, entonces se ejecutarán las instrucciones especificadas en default. El
apartado default es opcional, en caso de no existir y de que la expresión del switch no
coincida con ninguno de los valores case, entonces no se hace nada.

En el siguiente ejemplo el usuario introduce un número entero y el programa muestra el


dı́a de la semana correspondiente:

1 #include <stdio.h>
2

3 int main() {
4 int dia;
5

6 printf("Introduce el dı́a de la semana (1-7): ");


7 scanf(" %d", &dia);
8

9 switch( dia ) {
10 case 1: printf("LUNES\n"); break;
11 case 2: printf("MARTES\n"); break;
12 case 3: printf("MIERCOLES\n"); break;
13 case 4: printf("JUEVES\n"); break;
14 case 5: printf("VIERNES\n"); break;
15 case 6: printf("SABADO\n"); break;
16 case 7: printf("DOMINGO\n"); break;
17 default: printf("Dı́a incorrecto\n");
18 }
19 return 0;
20 }

En este ejemplo la instrucción switch evalúa el valor de la variable dia. A continuación


busca un case cuyo valor coincida con el de esta variable y, si lo encuentra, ejecuta las
40
3.1 Sentencias de selección

instrucciones correspondientes. Si el valor de la variable dia no coincide con ninguno de


los case, entonces se ejecuta el bloque default.

Debe tenerse en cuenta que si se encuentra un case que coincida con el valor de la expre-
sión (en este ejemplo dia), se ejecutan todas las instrucciones que aparezcan a continua-
ción, hasta que se alcance una instrucción break. Esto quiere decir que si en el ejemplo
anterior no se hubieran incluido las instrucciones break y el usuario introduce, por ejem-
plo, un 6, se hubiera ejecutado no sólo la instrucción printf("SABADO\n") sino tam-
bién las instrucciones printf("DOMINGO\n") y printf("Dı́a incorrecto\n"). Lo
habitual será, por tanto, que cada grupo de instrucciones especificados en un case termi-
ne con la instrucción break. Sin embargo, tal y como se muestra en el ejemplo siguiente,
habrá ocasiones en las que interese no incluir la instrucción break.

1 #include <stdio.h>
2

3 int main() {
4 int curso;
5

6 printf("Introduce el curso en el que te encuentras: ");


7 scanf(" %d", &curso);
8

9 printf("Asignaturas que todavı́a debes cursar:\n");


10

11 switch( curso ) {
12 case 1: printf("PROGRAMACIÓN\n");
13 case 2: printf("ALGORÍTMICA\n");
14 case 3: printf("PROGRAMACIÓN AVANZADA\n");
15 case 4: printf("PROGRAMACIÓN DE REDES\n");
16 case 5: printf("INGENIERIA DEL SOFTWARE\n"); break;
17 default: printf("Curso incorrecto\n");
18 }
19 return 0;
20 }

En el ejemplo anterior, si el usuario introduce, por ejemplo, un 2, el programa mostrará:

Asignaturas que todavı́a debes cursar:


ALGORÍTMICA
PROGRAMACIÓN AVANZADA
PROGRAMACIÓN DE REDES
INGENIERIA DEL SOFTWARE

Obsérvese que, una vez se entra en un case, se ejecuta el resto de instrucciones hasta
alcanzar una instrucción break.
41
Capı́tulo 3. Estructuras de control

3.2 Sentencias de repetición


Las sentencias de repetición permiten ejecutar un bloque de instrucciones más de una
vez, esto es, permiten hacer bucles. En lenguaje C se pueden implementar bucles de tres
modos distintos, mediante las sentencias while, do-while y for.

3.2.1 La sentencia while


Para implementar un bucle while se utiliza la siguiente sintaxis:

while( expresion ) {
instruccion_1;
instruccion_2;
. . .
instruccion_N;
}

Mientras la expresión de la instrucción while sea cierta, se ejecutarán las instrucciones


contenidas en el bucle. Cuando se ejecuta la última instrucción del bucle (instruccion_N)
se vuelve a evaluar de nuevo la expresión y, si sigue siendo cierta, se ejecutan de nuevo
todas las instrucciones. Se denomina iteración a cada una de las repeticiones.

Al igual que ocurre con la instrucción if-else, si el bucle contiene una única instrucción,
las llaves se pueden omitir.

El siguiente ejemplo muestra 10 veces el texto “Hola mundo” y a continuación una vez el
texto “Fin del programa”.

1 #include <stdio.h>
2

3 int main() {
4 int i = 0;
5 while( i < 10 ) {
6 printf("Hola mundo\n");
7 i++;
8 }
9 printf("Fin del programa\n");
10 return 0;
11 }

Como puede observarse, en este ejemplo se ha utilizado la variable i a modo de contador,


para controlar el número de veces que queremos que se repita el bucle (número de itera-
ciones). En este caso, el bucle se repite 10 veces ya que en cada iteración la variable i se

42
3.2 Sentencias de repetición

incrementa en uno. Cuando ésta variable valga 10, la condición i < 10 será falsa, con lo
que ya no se entrará de nuevo en el bucle.

Para evitar bucles infinitos es imprescindible que alguna de las instrucciones del bucle
modifique de algún modo la expresión de la sentencia while, de lo contrario, una vez
se entra en el bucle ya no se puede salir del mismo. El siguiente programa muestra un
ejemplo de un bucle infinito:

1 #include <stdio.h>
2

3 int main() {
4 int i = 0;
5 while( i < 10 ) { // Esto siempre va a ser cierto
6 printf("Hola mundo\n");
7 }
8 return 0;
9 }

En el siguiente ejemplo se muestra un programa que solicita dos números y una operación
(suma, resta, multiplicación o división) y realiza la operación especificada. A continua-
ción pregunta si se desea hacer otra operación. En caso afirmativo se repite de nuevo todo
el proceso.

1 #include <stdio.h>
2

3 int main() {
4 float a, b, c;
5 int operacion, repetir;
6

7 repetir = 1; // Para forzar la entrada en el bucle la primera


vez
8 while( repetir == 1 ) {
9 printf("Introduce dos números: ");
10 scanf(" %f %f", &a, &b);
11 printf("1.Sumar \n2.Restar \n3.Multiplicar \n4.Dividir \n")
;
12 scanf(" %d", &operacion);
13 switch( operacion ) {
14 case 1: c = a+b; break;
15 case 2: c = a-b; break;
16 case 3: c = a*b; break;
17 case 4: c = a/b; break;
18 default: printf("Operación incorrecta\n"); c=0;
19 }
20 printf("El resultado de la operación es %f\n", c);
21 printf("Deseas hacer otra operación? (1=SI / 2=NO) ");
43
Capı́tulo 3. Estructuras de control

22 scanf(" %d", &repetir);


23 }
24 return 0;
25 }

Puede ocurrir que el contenido de un bucle while no se ejecute nunca. El siguiente pro-
grama solicita números enteros hasta que se introduzca un cero y al final muestra cuántos
de ellos eran positivos.

1 #include <stdio.h>
2

3 int main() {
4 int num, positivos;
5

6 positivos = 0; // De momento no se ha introducido ningún


número positivo
7 printf("Introduce un número entero: ");
8 scanf(" %d", &num);
9 while( num != 0 ) {
10 if( num > 0 )
11 positivos++;
12 printf("Introduce otro número: ");
13 scanf(" %d", &num); // Este número se utilizará en la
siguiente iteración
14 }
15 printf("Has introducido %d números positivos\n", positivos);
16 return 0;
17 }

En el ejemplo anterior, si el primer número introducido es cero, no llega a entrarse en el


bucle while. En este caso la variable positivos se queda con su valor inicial cero. En
caso de que se entre en el bucle, al final del mismo se pide un nuevo número que, en caso
de que sea distinto de cero, provocará que se entre de nuevo en el bucle.

3.2.2 La sentencia do-while


La sintaxis del bucle do-while es la siguiente:

do {
instruccion_1;
instruccion_2;
. . .
instruccion_N;
} while( expresion );
44
3.2 Sentencias de repetición

La construcción de un bucle do-while es muy similar a la de un bucle while. La única


diferencia es que en el bucle while primero se comprueba la expresión y, si es cierta, se
ejecutan las instrucciones del bucle, mientras que en el caso del bucle do-while primero
se ejecutan las instrucciones del bucle y al final se evalúa la expresión. Si ésta resulta ser
cierta, entonces se ejecutarán de nuevo todas las instrucciones, ası́ repetidas veces hasta
que la expresión sea falsa. Vemos por tanto que en un bucle do-while se hará como
mı́nimo una iteración.

Cualquier código escrito con un bucle do-while puede reescribirse mediante un bucle
while y viceversa. Por ejemplo, el siguiente bucle do-while

1 printf("Introduce un número positivo:");


2 do {
3 scanf(" %d", &num);
4 } while( num <= 0 );

puede reescribirse como:

1 printf("Introduce un número positivo:");


2 scanf(" %d", &num);
3 while( num <= 0 ) {
4 scanf(" %d", &num);
5 }

En el siguiente ejemplo se solicitan dos números enteros a y b y se dividen. Para evitar


una división por cero, mediante un bucle do-while nos aseguramos de que el valor de b
sea distinto de cero (si b vale cero, solicitamos los datos de nuevo).

1 #include <stdio.h>
2

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

6 do {
7 printf("Introduce dos números (el segundo de ellos distinto
de cero) ");
8 scanf(" %f %f", &a, &b);
9 } while( b == 0 );
10 printf(" %f / %f = %f\n", a, b, a/b);
11 return 0;
12 }

45
Capı́tulo 3. Estructuras de control

En este ejemplo, si el valor introducido en b es cero, entonces la condición b == 0 es


cierta y, en consecuencia, se piden los datos de nuevo. En general se puede emplear un
bucle do-while con la siguiente sintaxis para validar los datos de entrada:

do {
solicitar datos
} while( datos incorrectos );

Otro uso habitual del bucle do-while es para la implementación de un menú de opciones.
A continuación se muestra un ejemplo que presenta un menú, de modo que el programa
se ejecuta repetidas veces hasta que se escoge la opción “Salir”:

1 #include <stdio.h>
2

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

7 do {
8 printf("1.- Sumar\n");
9 printf("2.- Restar\n");
10 printf("3.- Salir\n");
11 printf("Elige una opción: ");
12 scanf(" %d", &opc);
13 printf("Introduce dos números: ");
14 scanf(" %f %f", &a, &b);
15 if( opc == 1 ) printf(" %f + %f = %f\n", a, b, a+b);
16 else if( opc == 2 ) printf(" %f - %f = %f\n", a, b, a-b);
17 else if( opc == 3 ) printf("Adiós\n");
18 else printf("Opción incorrecta\n");
19 } while( opc != 3 );
20 return 0;
21 }

En este caso el programa presenta un menú con dos opciones, más una tercera para finali-
zar. El bucle do-while provoca que el programa se ejecute repetidas veces hasta que se
escoja la opción 3 (salir). Cualquier otro valor distinto de 3 provoca que el bucle se repita
de nuevo. El grupo de instrucciones if-else se podrı́a haber resuelto también mediante
una sentencia switch, tal y como se ha mostrado en ejemplos anteriores.

46
3.2 Sentencias de repetición

3.2.3 La sentencia for


La sintaxis del bucle for es la siguiente:

for(exp1; exp2; exp3) {


instruccion_1;
instruccion_2;
. . .
instruccion_N;
}

Las llaves, como siempre, son opcionales si el bucle contiene una única instrucción. Por
otro lado, exp1, exp2 y exp3 pueden ser cualquier expresión válida en C. De forma más
detallada diremos que:

exp1: se ejecuta una única vez, antes de que comiencen a ejecutarse las instruccio-
nes del bucle. Suele utilizarse para inicializar alguna variable.

exp2: es la condición de entrada al bucle. Mientras exp2 se evalúe a cierto, se


ejecutarán las instrucciones del bucle.
exp3: se ejecuta al final de cada iteración (después de instruccion_N). Suele
utilizarse para modificar las variables que intervienen en la condición de entrada
(exp2).

Por ejemplo, el siguiente programa muestra 10 veces el texto ”Hola Mundo”:

1 #include <stdio.h>
2

3 int main() {
4 int i;
5 for( i=1; i<=10; i++ ) {
6 printf("Hola Mundo\n");
7 }
8 return 0;
9 }

En este ejemplo, i=1 se ejecuta una única vez antes de que comience el bucle. La ex-
presión i<=10 es la condición de entrada al bucle, mientras sea cierta se ejecutarán las
instrucciones de dentro del bucle. Por último la expresión i++ se ejecuta al finalizar cada
una de las iteraciones, después de la instrucción printf. En definitiva, la variable i se ha
utilizado a modo de contador para controlar el número de iteraciones. Cuando el bucle se
haya repetido 10 veces, la variable i valdrá 11 y por tanto finalizará.

47
Capı́tulo 3. Estructuras de control

El bucle for del ejemplo anterior es equivalente al siguiente bucle while:

1 i = 1;
2 while( i <= 10 ) {
3 printf("Hola Mundo\n");
4 i++;
5 }

En general, cualquier bucle for del tipo

for(exp1; exp2; exp3) {


instruccion_1;
. . .
instruccion_N;
}

puede expresarse mediante un bucle while del siguiente modo:

exp1;
while(exp2) {
instruccion_1;
. . .
instruccion_N;
exp3;
}

En un bucle for puede omitirse cualquiera de las expresiones. Por ejemplo, el siguiente
programa muestra n veces el texto ”Hola Mundo”, donde n es un valor introducido por el
usuario. Obsérvese que en el bucle for se ha omitido exp1 ya que en este caso la variable
n ya tiene un valor inicial:

1 #include <stdio.h>
2

3 int main() {
4 int n;
5 printf("Introduce un valor entero: ");
6 scanf(" %d", &n);
7 for( ; n>0; n-- ) {
8 printf("Hola Mundo\n");
9 }
10 return 0;
11 }

Si la que se omite es exp2 entonces se considera que la condición de entrada es siempre


cierta, con lo que se podrı́a generar un bucle infinito.
48
3.2 Sentencias de repetición

También es posible que alguna de las expresiones contenga más de una instrucción. En
este caso, dichas instrucciones deben separarse por coma, tal y como se muestra en el
siguiente ejemplo:

1 #include <stdio.h>
2

3 int main() {
4 int i, j;
5 for( i=0, j=10; i<j; i++, j-- ) {
6 printf(" %d + %d = %d\n", i, j, i+j);
7 }
8 return 0;
9 }

En este ejemplo se inicializan las variables i y j a 0 y 10 respectivamente, y al final


de cada iteración se incremente el valor de i y se decrementa el de j. Por tanto, este
programa producirı́a la siguiente salida:

1 + 10 = 11
2 + 9 = 11
3 + 8 = 11
4 + 7 = 11
5 + 6 = 11

Este mismo programa escrito con un bucle while serı́a:

1 #include <stdio.h>
2

3 int main() {
4 int i, j;
5 i = 0;
6 j = 10;
7 while( i<j ) {
8 printf(" %d + %d = %d\n", i, j, i+j);
9 i++;
10 j--;
11 }
12 return 0;
13 }

49
Capı́tulo 3. Estructuras de control

3.2.4 Bucles anidados


Es posible que alguna de las instrucciones de un bucle sea, a su vez, otro bucle. Esto es lo
que se conoce como un bucle anidado. Por ejemplo, el siguiente programa:

1 #include <stdio.h>
2

3 int main() {
4 int i, j;
5 for( i=1; i<=3; i++ ) {
6 printf("Bucle externo, iteración %d\n", i);
7 for( j=1; j<=2; j++ ) {
8 printf("\tBucle interno, iteración %d- %d\n", i, j);
9 }
10 printf("-------------------------------------\n");
11 }
12 return 0;
13 }

generarı́a la siguiente salida por pantalla:

Bucle externo, iteración 1


Bucle interno, iteración 1-1
Bucle interno, iteración 1-2
-------------------------------------
Bucle externo, iteración 2
Bucle interno, iteración 2-1
Bucle interno, iteración 2-2
-------------------------------------
Bucle externo, iteración 3
Bucle interno, iteración 3-1
Bucle interno, iteración 3-2
-------------------------------------

Como puede observarse, el bucle externo se repite tres veces y, en cada una de estas
iteraciones, el bucle interno se repite dos veces.

50
3.3 Algunas técnicas útiles: contadores, acumuladores y banderas

3.3 Algunas técnicas útiles: contadores, acumuladores y


banderas
Existen algunos modos particulares y ampliamente utilizados de trabajar con variables,
que nos permiten implementar los conceptos de contador, acumulador y bandera. A
continuación se describen estos conceptos:

3.3.1 Contadores
Utilizamos el término contador para referirnos a una variable cuyo valor se va incremen-
tando de uno en uno bajo ciertas condiciones. Este tipo de variables deben inicializarse
con algún valor (normalmente cero).

Por ejemplo, el siguiente programa solicita la introducción de 10 números enteros y cal-


cula cuántos de ellos son pares:

1 #include <stdio.h>
2

3 int main() {
4 int i, cont, num;
5 cont = 0; // Inicializamos el contador
6 for( i=1; i<=10; i++ ) {
7 printf("Introduce un número entero: ");
8 scanf(" %d", &num);
9 if( num %2 == 0 )
10 cont++; // Incrementamos el contador
11 }
12 printf("Has introducido %d números pares\n", cont);
13 return 0;
14 }

En este ejemplo la variable cont funciona a modo de contador, de modo que se iniciali-
za a cero y, cada vez que se detecta la entrada de un número par, se incrementa en uno.
La variable i del bucle for también es un contador, en este caso empleado para contro-
lar el número de iteraciones que realiza el bucle. Este contador se inicializa a uno y se
incrementa tras cada iteración del bucle.

51
Capı́tulo 3. Estructuras de control

3.3.2 Acumuladores
Otro uso habitual de las variables consiste en añadirles cierto valor al que ya tenı́an de
alguna operación anterior, mediante operaciones del tipo

variable += valor;

Por ejemplo, la instrucción

1 i += 5;

incrementa en 5 el valor de i.

Al igual que los contadores, los acumuladores deben ser inicializados. De hecho un con-
tador es un tipo particular de acumulador, al que se le van sumando unidades.

El siguiente ejemplo calcula la media aritmética de 10 valores. Para ello se emplea un


acumulador para obtener, en primer lugar, la suma de todos ellos.

1 #include <stdio.h>
2

3 int main() {
4 int i;
5 float num, suma;
6 suma = 0; // Inicializamos el acumulador
7 for( i=1; i<=10; i++ ) {
8 printf("Introduce un número: ");
9 scanf(" %f", &num);
10 suma += num; // Incrementamos el acumulador en ’
num’ unidades
11 }
12 printf("Media = %f\n", suma/10);
13 return 0;
14 }

En este ejemplo se puede observar que en la variable suma se van acumulando todos los
valores introducidos en la variable num.

También es posible utilizar un acumulador en modo producto con una instrucción del tipo

variable *= valor;

Por ejemplo, la instrucción

1 i *= 5;

52
3.3 Algunas técnicas útiles: contadores, acumuladores y banderas

multiplica por 5 el valor que hubiese en i y almacena el resultado en la propia variable i.

Los acumuladores en forma de producto se inicializan habitualmente a 1. Por ejemplo,


el factorial de un número n (1*2*3* ... *n) se puede calcular fácilmente empleando un
acumulador en forma de producto. En este caso el acumulador deberı́a inicializarse con
valor 1 e ir multiplicándose cada vez por los valores 2,3,...,n tal y como se muestra en el
siguiente ejemplo:

1 #include <stdio.h>
2

3 int main() {
4 int i, n, fact;
5 fact = 1; // Inicializamos el acumulador
6 printf("Introduce un número: ");
7 scanf(" %d", &n);
8 for( i=2; i<=n; i++ )
9 fact *= i; // Multiplicamos ’i’ a lo que hay en el
acumulador
10

11 printf("Factorial de %d = %d\n", n, fact);


12 return 0;
13 }

Debe observarse que cuando se utiliza un acumulador en forma de producto, lo habitual


será inicializarlo a uno (nunca debe inicializarse a cero ya que en este caso no cambiarı́a
su valor con las sucesivas multiplicaciones).

3.3.3 Banderas
Una bandera o flag es una variable que toma únicamente dos valores (1/0, verdadero/falso)
y que se emplea para comprobar si se ha producido una determinada condición durante
la ejecución del programa. La variable se inicializa a 0 (falso) o a 1 (verdadero) y, si se
cumplen ciertas condiciones en un instante determinado, se cambia su valor. Finalmente
se chequea la variable para ver si su valor ha cambiado.

Por ejemplo, imaginemos que se desea hacer un programa que sume el precio de 10
productos y, además, queremos sacar un mensaje de advertencia si alguno de los precios
introducidos es negativo. La suma se desea calcular igualmente, pero en caso de que se
detecte algún precio negativo, se mostrará al final del programa un mensaje a modo de
advertencia. Esto puede resolverse con el uso de una bandera tal y como se muestra a
continuación:

53
Capı́tulo 3. Estructuras de control

1 #include <stdio.h>
2

3 int main() {
4 int i, flag;
5 float pvp, suma;
6 suma = 0; // Inicializamos el acumulador
7 flag = 0; // Inicializamos la bandera
8 for( i=1; i<=10; i++ ) {
9 printf("Introduce precio: ");
10 scanf(" %f", &pvp);
11 if( pvp < 0 ) // Si el número es negativo...
12 flag = 1; // nos lo anotamos en el flag
13 suma += pvp; // En cualquier caso lo sumamos
14 }
15 printf("Suma = %f\n", suma);
16 if( flag == 1 ) // Comprobamos si el flag ha
cambiado
17 printf("ATENCIÓN, se han introducido precios negativos!\n")
;
18 return 0;
19 }

Como puede observarse, al flag se le da un valor inicial (0 en este caso) y, si detectamos


algún número negativo, lo anotamos cambiando el valor original del flag. Bastará con que
haya un solo número negativo para que el flag no conserve su valor inicial. Al final del
proceso se chequea el valor del flag para comprobar si habı́a algún número negativo.

Continuando con el ejemplo anterior, un error habitual con el uso de banderas es el si-
guiente:

1 flag = 0
2 for( i=1; i<=10; i++ ) {
3 printf("Introduce precio: ");
4 scanf(" %f", &pvp);
5 if( pvp < 0 )
6 flag = 1;
7 else
8 flag = 0;
9 }

Obsérvese que la sentencia else no es correcta. Este código provoca que la variable flag
se quede con valor 0 o 1, dependiendo del signo del último valor introducido. Esto es,
con el código anterior, si el último número introducido es positivo, el flag quedará con
valor 0, independientemente de que los números anteriores fueran positivos o negativos.
54
3.4 Ejercicios resueltos

3.4 Ejercicios resueltos


1. Escribir un programa que realice la división de dos números, teniendo en cuenta la
posibilidad de que el divisor pueda ser cero.
SOLUCIÓN:

1 #include <stdio.h>
2

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

6 printf("Introduce dividendo: ");


7 scanf(" %f", &a);
8 printf("Introduce divisor: ");
9 scanf(" %f", &b);
10 if( b == 0 )
11 printf("Error: división por cero\n");
12 else
13 printf(" %f\n", a/b);
14 return 0;
15 }

2. Escribir un programa que solicite un número entero y muestre la tabla de multiplicar


de dicho número.
SOLUCIÓN:

1 #include <stdio.h>
2

3 int main() {
4 int i, n;
5

6 printf("Introduce un número: ");


7 scanf(" %d", &n);
8 for( i=1; i<=10; i++ )
9 printf(" %d x %d = %d\n", n, i, n*i);
10

11 return 0;
12 }

3. Escribir un programa que muestre las tablas de multiplicar del 1 al 10.


SOLUCIÓN:

1 #include <stdio.h>
55
Capı́tulo 3. Estructuras de control

3 int main() {
4 int i, j;
5

6 for( i=1; i<=10; i++ ) {


7 printf("TABLA DE MULTIPLICAR DEL %d\n", i);
8 for( j=1; j<=10; j++ ) {
9 printf(" %d x %d = %d\n", i, j, i*j);
10 }
11 }
12 return 0;
13 }

4. Escribir un programa que calcule la media de n valores. El número de datos a pro-


mediar (n) deberá ser introducido por el usuario. Además, deberá comprobarse que
el valor de n introducido sea mayor que cero, de lo contrario volverá a solicitarse
tantas veces como sea necesario.
SOLUCIÓN:

1 #include <stdio.h>
2

3 int main() {
4 int i, n;
5 float num, suma;
6

7 // Pedimos valor de n, asegurándonos que no sea cero


8 do {
9 printf("Introduce la cantidad de valores a promediar: "
);
10 scanf(" %d", &n);
11 if( n <= 0 )
12 printf("Debes introducir un valor mayor que cero\n")
;
13 } while( n <= 0 );
14

15 // Pedimos n números y los sumamos


16 suma = 0;
17 for( i=1; i<=n; i++ ) {
18 printf("Introduce valor %d: ", i);
19 scanf(" %f", &num);
20 suma += num;
21 }
22 printf("Media = %f\n", suma/n);
23 return 0;
24 }

56
3.5 Ejercicios propuestos: condicionales

3.5 Ejercicios propuestos: condicionales

1. Dada una ecuación de segundo grado de la forma aX 2 + bX + c = 0, implementar


un programa que pida los coeficientes√a, b y c, y muestre por pantalla las dos so-
luciones de la ecuación (X = (−b ± b2 − 4ac)/(2a)). Deberá comprobarse que
b2 − 4ac ≥ 0 y que a = 0.
Nota: Para calcular la raı́z cuadrada, se puede utilizar la función sqrt definida en la
librerı́a math.h .

2. Una empresa de transportes cobra 30 euros por cada bulto que transporta. Además,
si el peso total de todos los bultos supera los 300 kilos, cobra 0.9 euros por cada
kilo extra. Por último, si el transporte debe realizarse en sábado, cobra un plus de
60 euros. La empresa no realiza el pedido si hay que transportar más de 30 bultos,
si el peso total supera los 1.000 kilos o si se solicita hacerlo en domingo. Se pide
realizar un programa que solicite el número de bultos, el peso total y el dı́a de la
semana (número entre 1 y 7) y muestre el precio en caso de que pueda realizarse el
transporte, o un mensaje de advertencia en caso contrario.

3.6 Ejercicios propuestos: bucles

Aprendiendo a utilizar contadores


3. Mostrar 10 veces por pantalla el texto “Hola mundo”.

4. Mostrar N veces por pantalla el texto “Hola mundo”. Pedir el valor de N al inicio
del programa.

5. Repetir el ejercicio anterior, numerando cada una de las lı́neas. Por ejemplo, si N
vale 10, el programa deberá mostrar:

1. Hola mundo
2. Hola mundo
. . .
10. Hola mundo

6. Repetir el ejercicio anterior, numerando las lı́neas en orden decreciente, esto es:

10. Hola mundo


9. Hola mundo
. . .
1. Hola mundo

57
Capı́tulo 3. Estructuras de control

7. Mostrar el valor del seno(x) para valores de x comprendidos entre 0 y 2, con in-
crementos de 0,1. El seno se puede calcular mediante la función sin(x), lo que
requiere incluir la librerı́a math.h. La salida por pantalla deberá tener el siguiente
aspecto:

X sen(x)
------- --------
0.000000 0.000000
0.100000 0.099833
0.200000 0.198669
. . . . . .
2.000000 0.909297

8. Introducir 10 números y decir cuántos de ellos se encuentran en el rango [50−100].

Aprendiendo a utilizar acumuladores


9. Solicitar 10 valores y calcular la suma de todos ellos.

10. Solicitar 10 valores y mostrar la suma total de los pares por un lado y la de los
impares por otro.
10

11. Calcular 1 ∗ 2 ∗ · · · ∗ 10, esto es, i.
i=1

12. Escribir un programa que calcule el producto de dos números enteros positivos sin
utilizar la operación ’*’ (el producto deberá calcularse a base de sumas).

13. Solicitar dos números enteros positivos a y b y calcular a/b y a %b, sin utilizar las
operaciones “/” y “ %”.
Pista: Contar el número de veces que se puede restar b de a.
14. Solicitar un número N y calcular el factorial de N. Comprobar que el número intro-
ducido es positivo y menor o igual que 20. En caso contrario solicitarlo de nuevo,
hasta que el valor de N sea correcto.

15. Dado el siguiente fragmento de código, explicar qué operación realiza. Hacer una
traza suponiendo inicialmente a = 5.

1 valor = 0;
2 while( a > 0 ) {
3 valor += a--;
4 }
5 printf("El resultado es %d\n", valor);

58
3.6 Ejercicios propuestos: bucles

Aprendiendo a utilizar flags


17. Solicitar un número y decir si es o no primo. Para ello, dado el número n se deben
realizar las operaciones n%2, n%3, n%4... Si alguna de estas operaciones da cero
quiere decir que la división era exacta y por tanto n no es primo. En caso contrario,
será primo.

18. Solicitar 10 números y decir si están ordenados crecientemente, esto es, si cada
nuevo valor introducido es mayor que el anterior.
Pista: Utiliza una variable para almacenar el último valor introducido y otra para
almacenar el penúltimo y, si ultimo < penultimo, activa un flag.

Juegos
19. Escribir un programa que juegue a “adivinar un número” del siguiente modo: El
usuario piensa un número entre 1 y 100 y el ordenador debe averiguar dicho núme-
ro. Para ello, cada vez que el ordenador sugiere un número, el usuario debe contestar
con el carácter g, p o c en función de que el número sea “demasiado grande”, “de-
masiado pequeño” o “correcto”. Se debe implementar un algoritmo eficiente que
adivine el número en el menor número de intentos posible.

20. Se desea implementar un juego con las siguientes reglas:


Se parte de una cantidad inicial de 17 fichas. Dos jugadores, de forma alterna, van
retirando fichas del montón, permitiéndose en cada jugada retirar 1, 2 o 3 fichas.
Pierde el jugador que retira la última ficha del montón.
Implementar un programa que simule el juego descrito, de modo que se juegue
contra el ordenador. Empezará retirando fichas el jugador humano, de modo que el
ordenador seguirá en todo momento la estrategia de retirar 4 − x fichas, siendo x
las fichas que retiró el otro jugador en el turno anterior. Jugando de este modo, el
ordenador siempre ganará la partida. Cada vez que sea el turno del ordenador, éste
informará de cuántas fichas retira y cuántas quedan en el montón. A continuación
se le pedirá al usuario que introduzca el número de fichas que desea retirar, y se
mostrará por pantalla igualmente cuántas fichas quedan en el montón. Este proceso
se repetirá hasta que finalice la partida.

59
Capı́tulo 4

Funciones

4.1 Introducción
Los programas que hemos creado hasta ahora no son modulares, todo el código del pro-
grama aparece dentro de main. Esto puede ser adecuado para programas pequeños y
sencillos, pero cuando se abordan programas de mayor envergadura y complejidad es ne-
cesario modularizarlos, esto es, dividir el programa en bloques menores. En el lenguaje
C, esta división se hace mediante el uso de funciones.

Estudiaremos en primer lugar el uso de las funciones existentes en la librerı́a de C, para


aprender más adelante cómo crear nuestras propias funciones.

4.2 Funciones de la librerı́a de C


Cualquier compilador de C viene acompañado de una serie de funciones que realizan
operaciones de uso frecuente. Algunas de ellas ya las hemos venido utilizando. Por ejem-
plo, existen funciones como scanf y printf que permiten la entrada/salida de datos,
o funciones matemáticas como sqrt, sin, cos, etc. que permiten realizar cálculos ma-
temáticos. El listado completo de funciones es realmente extenso y no es objetivo nuestro
detallarlo.

Aunque ya hemos hecho uso de las funciones en capı́tulos anteriores, conviene observar
con mayor detalle el modo de utilizarlas. A continuación se muestra un ejemplo sencillo:

1 #include <stdio.h>
2 #include <math.h>
3

61
Capı́tulo 4. Funciones

4 int main() {
5 float x, y;
6

7 printf("Introduce un valor: ");


8 scanf(" %f", &x);
9 y = pow(x, 2);
10 printf(" %.2f elevado a 2 = %f\n", x, y);
11

12 return 0;
13 }

En este ejemplo printf, scanf y pow son funciones de la librerı́a de C. Lo primero que
debemos saber es que las funciones de C se agrupan en categorı́as. Por ejemplo printf
y scanf son funciones de entrada/salida, mientras que pow es una función matemática.
Para poder utilizar las funciones de C es necesario incluir cierta información en nuestro
programa. Esta información se encuentra almacenada en lo que se conoce como ficheros
de cabecera o header files y se incluye mediante la instrucción:

#include <nombre_del_fichero_de_cabecera>

En la sección 4.3.5 se explicará con mayor detalle los ficheros de cabecera. De momento
nos basta con saber que es necesario incluirlos. En el ejemplo anterior stdio.h hace
referencia al fichero de cabecera standar input/output (entrada/salida estándar). Hemos
incluido este fichero ya que hacemos uso de las funciones printf y scanf, las cuales
se agrupan bajo esta categorı́a. De forma similar, dado que nuestro programa utiliza la
función matemática pow, es necesario incluir el fichero de cabecera math.h.

Si nos centramos en la sintaxis, se puede observar que para llamar o invocar a una fun-
ción debemos poner el nombre de la función seguido de una lista de argumentos. Estos
argumentos representan la información que le pasamos a la función y deben ir encerrados
entre paréntesis y separados por coma:

nombre_funcion(argumento1, argumento2, ... , argumentoN)

En el ejemplo anterior podemos observar que en las llamadas a las funciones scanf
y pow contienen dos argumentos. Por otro lado, la función printf, se puede invocar
con un número variable de argumentos1 . En el ejemplo, el primer printf tiene un solo
argumento, mientras que el segundo tiene tres.

También hay funciones que no tienen argumentos, como getchar(). Aun en este caso,
los paréntesis deben estar presentes.
1 En realidad scanf también tiene un número variable de argumentos. Por ejemplo scanf(” %f %f”, &a, &b)

tiene tres argumentos.

62
4.3 Creando nuestras propias funciones

Cuando se invoca a una función, el programa realiza las operaciones necesarias para eje-
cutar la función en cuestión, teniendo en cuenta los argumentos especificados. Algunas
funciones (en realidad la mayorı́a) además de requerir argumentos con cierta información,
también devuelven un valor, resultante de la operación realizada. En el ejemplo anterior
la función pow requiere dos argumentos y devuelve un valor (en este caso el resultado de
elevar el segundo argumento al primero). Cuando una función devuelve un valor, ésta pue-
de ser utilizada como parte de una expresión. Por ejemplo, lo siguiente serı́an expresiones
válidas en C:

y = pow(x, 2);

if( pow(x, 2) > 1 ) { ...

printf("%.2f elevado a 2 = %f\n", x, pow(x, 2));

Por otro lado, los argumentos de una función pueden ser también expresiones más com-
plejas, tal y como se muestra en los siguientes ejemplos:

z = pow(3*x, 2*y);

z = pow(sqrt(x)*2, 1.0/y);

4.3 Creando nuestras propias funciones


Hasta ahora los programas que hemos creado contienen todas las instrucciones en main.
Esto es adecuado para programas muy pequeños, pero para programas de mayor compleji-
dad este modo de trabajar presenta grandes inconvenientes: implementación complicada,
imposibilidad de reutilizar el código creado y código difı́cil de entender.
 
Imaginemos que queremos realizar un programa que calcule la operación m n , esto es, el
número de combinaciones de m elementos tomados de n en n. Sabiendo que
 
m m!
=
n n!(m − n)!
y teniendo en cuenta que el lenguaje C no dispone de ninguna función  propia para el
cálculo del factorial, podrı́amos desarrollar un programa que calcule m
n del siguiente
modo:

1 #include <stdio.h>
2 int main() {
3 int m, n, i, fact_m, fact_n, fact_m_n, res;
4

5 // Pedir datos
6 do {
63
Capı́tulo 4. Funciones

7 printf("Introduce dos valores m, n, positivos, con m>=n ");


8 scanf(" %d %d", &m, &n);
9 } while (m<0 || n<0 || m<n);
10

11 // Calcular m!
12 fact_m = 1;
13 for(i=1; i<=m; i++)
14 fact_m = fact_m * i;
15

16 // Calcular n!
17 fact_n = 1;
18 for(i=1; i<=n; i++)
19 fact_n = fact_n * i;
20

21 // Calcular (m-n)!
22 fact_m_n = 1;
23 for(i=1; i<=(m-n); i++)
24 fact_m_n = fact_m_n * i;
25

26 // Calcular número de combinaciones y mostrar resultado


27 res = fact_m / (fact_n*fact_m_n);
28 printf("Número combinaciones = %d\n", res);
29

30 return 0;
31 }

Aunque el programa anterior es correcto, la implementación no es la más adecuada ya


que estamos repitiendo un código muy similar en varios puntos del programa (el cálculo
del factorial). Lo ideal serı́a disponer de una función que devolviese el factorial de un
número dado, con lo que el programa anterior podrı́a simplificarse tal y como se muestra
a continuación:

1 #include <stdio.h>
2 int main() {
3 int m, n, res;
4

5 // Pedir datos
6 do {
7 printf("Introduce dos valores m, n, positivos, con m>=n ");
8 scanf(" %d %d", &m, &n);
9 } while (m<0 || n<0 || m<n);
10

11 // Calcular número de combinaciones y mostrar resultado


12 res = fact(m) / (fact(n)*fact(m-n));
13 printf("Número combinaciones = %d\n", res);
14

64
4.3 Creando nuestras propias funciones

15 return 0;
16 }

Desafortunadamente el lenguaje C no dispone de la función fact, sin embargo es posible


crearla, tal y como veremos más adelante. Mediante la creación de funciones consegui-
mos desarrollar programas mejor estructurados. La programación modular o estructurada
permite:

Dividir un problema complejo en subproblemas más fáciles de resolver: divide y


vencerás.
Construir y probar por separado los subprogramas (funciones).
Reutilizar los módulos ya creados, evitando replicar código de manera innecesaria.
Facilitar el mantenimiento de los programas.

4.3.1 Cuestiones sintácticas


Para crear nuestras propias funciones empleamos la siguiente sintaxis:

tipo_dato nombre_funcion(lista_parametros) {
/* Cuerpo de la función */
}

donde:

tipo_dato indica el tipo de dato del valor que devuelve la función (int, foat,
char, etc.). En caso de que la función no devuelva ningún valor, el tipo de dato
deberá ser void.
nombre_funcion es el nombre que queremos darle a nuestra función, y por tanto
el que deberá utilizarse cuando queramos invocarla.
lista_parametros es un conjunto de pares tipo_dato nombre_variable se-
parados por comas. La lista de parámetros utilizada a la hora de implementar la
función debe coincidir en número y tipo con la lista de argumentos empleada en la
llamada a dicha función.

El tipo de dato devuelto por la función, junto con el nombre y la lista de parámetros es lo
que se conoce como cabecera de la función, mientras que el código que queda encerrado
entre la apertura y cierre de llave se denomina cuerpo de la función.

Por ejemplo, si quisiéramos crear una función que calcule el factorial de cierto número
entero, escribirı́amos:
65
Capı́tulo 4. Funciones

1 int fact( int n ) {


2 /* Instrucciones para calcular el factorial de n */
3 }

Esta función, de nombre fact, recibe un único parámetro de tipo int y de nombre n que
representa el valor del que queremos obtener el factorial y devuelve un valor de tipo int
(el resultado de calcular el factorial de n).

El siguiente ejemplo corresponderı́a a una función que calcula la media aritmética de tres
números enteros:

1 float media( int a, int b, int c ) {


2 /* Instrucciones para calcular la media de a, b, c */
3 }

En este caso la función tiene tres parámetros enteros que representan los números de los
que se desea obtener la media y devuelve un número real (float) que representa la media
calculada.

En general, un programa en C estará compuesto por varias funciones, donde necesaria-


mente una de ellas será la función main, tal y como se muestra en el siguiente ejemplo:

1 #include <stdio.h>
2

3 int funcion1( float a, float b ) {


4 . . .
5 }
6

7 void funcion2( int x ) {


8 . . .
9 }
10

11 int main() {
12 . . .
13 }

66
4.3 Creando nuestras propias funciones

4.3.2 Control de flujo y transferencia de la información


Cuando invocamos a una función, el control del flujo de ejecución se transfiere a di-
cha función, esto es, se ejecutan las instrucciones contenidas en el cuerpo de la función.
Cuando la función que ha sido invocada finaliza su ejecución, se devuelve el control del
flujo de ejecución al punto siguiente de la llamada.

La función invocada puede finalizar por dos motivos:

Porque se ha llegado a la última instrucción de la función.


Porque se ha ejecutado una instrucción return. La instrucción return sirve tanto
para finalizar la función en curso como para devolver un valor, tal y como veremos
más adelante.

Por otro lado, cuando invocamos a una función, puede existir (y normalmente existe)
una transferencia de información entre la función que realiza la llamada y la función
llamada. Esta transferencia de información se produce en dos instantes:

Al inicio de la llamada se copian los valores de los argumentos especificados en


la llamada (en caso de que existan) en los parámetros definidos en la función. Por
ejemplo, dado el siguiente código:

1 float media( int a, int b, int c ) {


2 /* Instrucciones para calcular la media de a, b, c */
3 . . .
4 }
5 int main() {
6 int num1, num2, num3, result;
7 . . .
8 result = media(num1, num2, num3);
9 . . .
10 }

cuando se realiza la llamada a la función media en la lı́nea 8, los valores de los


argumentos num1, num2 y num3 se copian en los parámetros a, b y c de la función
media (lı́nea 1). Es importante darse cuenta que el número y tipo de argumentos
utilizados en la llamada debe coincidir con el número y tipo de parámetros definidos
en la cabecera de la función. En este caso la función media tiene tres parámetros
de tipo entero y, en consecuencia, cuando llamemos a dicha función deberemos
proveer tres argumentos enteros. El valor de los argumentos se copia por orden de
aparición sobre los parámetros, de modo que en el ejemplo anterior, el valor de
num1 se copiará en la variable a, el de num2 en b y el de num3 en c.

67
Capı́tulo 4. Funciones

Al finalizar la llamada, en caso de que exista una sentencia return, el valor espe-
cificado a continuación de return es devuelto a la función que realizó la llamada.
Por ejemplo, dado el siguiente código:

1 float media( int a, int b, int c ) {


2 float m;
3 /* Instrucciones para calcular la media de a, b, c */
4 . . .
5 return m;
6 }
7 int main() {
8 int num1, num2, num3, result;
9 . . .
10 result = media(num1, num2, num3);
11 . . .
12 }

cuando se ejecuta la instrucción return m y, por tanto, finaliza la llamada a la


función media, el valor que tenga la variable m es devuelto. Esto provoca que la
llamada inicial media(num1, num2, num3) se evalúe al valor de la variable m.
Podrı́a verse como si toda la instrucción media(num1, num2, num3) se sustitu-
yera por el valor de m.

El programa anterior, escrito en su totalidad, quedarı́a como sigue:

1 #include <stdio.h>
2

3 float media( int a, int b, int c ) {


4 float m;
5 m = (a+b+c)/3.0;
6 return m;
7 }
8 int main() {
9 int num1, num2, num3, result;
10 printf("Introduce tres valores: ");
11 scanf(" %d %d %d", &num1, &num2, &num3);
12 result = media(num1, num2, num3);
13 printf("La media de %d %d y %d es %f\n", num1, num2, num3,
result);
14 }

68
4.3 Creando nuestras propias funciones

m 
Si retomamos el ejemplo del cálculo de n , el programa completo quedarı́a como sigue:

1 #include <stdio.h>
2

3 int fact( int num ) {


4 int i, f=1;
5 for( i=2; i<=num; i++ ) // Calculamos el factorial de num
6 f = f * i;
7 return f; // Devolvemos el factorial calculado
8 }
9

10 int main() {
11 int m, n, res;
12

13 // Pedir datos
14 do {
15 printf("Introduce dos valores m, n, positivos, con m>=n ");
16 scanf(" %d %d", &m, &n);
17 } while (m<0 || n<0 || m<n);
18

19 // Calcular resultado y mostrarlo


20 res = fact(m) / (fact(n)*fact(m-n));
21 printf("Número combinaciones = %d\n", res);
22

23 return 0;
24 }

En el programa mostrado como ejemplo existen tres llamadas a la función fact en la


lı́nea 20, con argumentos distintos en cada caso: fact(m), fact(n) y fact(m-n). Ello
provoca que en las distintas llamada a fact se copie cada vez un valor diferente en el
parámetro num: m en la primera llamada, n en la segunda y m-n en la tercera. En todos los
casos la función fact opera con el valor copiado en num. Este modo de trabajo permite
crear funciones genéricas que realicen ciertas operaciones con los valores que interese en
cada momento.

4.3.3 Parámetros y argumentos


Según la nomenclatura que hemos venido utilizando, denominamos parámetros a las
variables declaradas en la cabecera de las funciones. Estas variables reciben valores en
el momento en que se realiza una llamada a la función. Denominamos argumentos a
los valores especificados en la llamada a una función, los cuales son transferidos a los
parámetros correspondientes. Un argumento puede ser una variable, una constante o, en
general, una expresión.

69
Capı́tulo 4. Funciones

En ocasiones, en la literatura se utiliza también el término parámetro formal para refe-


rirse a los parámetros y parámetro actual para referirse a los argumentos. También es
frecuente que se utilice el término parámetro (sin más calificativos) para referirse, de
forma indistinta, tanto a los parámetros como a los argumentos.

4.3.4 Operaciones de Entrada/Salida en las funciones


Normalmente conviene que las operaciones de entrada/salida (printf/scanf) se en-
cuentren en la función principal. Por ejemplo, no serı́a adecuado resolver el programa
anterior del siguiente modo:

1 #include <stdio.h>
2

3 void media( int a, int b, int c ) {


4 float res;
5 res = (a+b+c)/3.0;
6 printf("MEDIA= %f\n", res);
7 }
8

9 int main() {
10 float num1, num2, num3;
11 printf("Introduce tres valores: ");
12 scanf(" %d %d %d", &num1, &num2, &num3);
13 media(num1, num2, num3);
14 return 0;
15 }

La diferencia de este programa con el anterior es que ahora el resultado lo muestra la


propia función media en lugar de hacerlo main. En este caso media es de tipo void ya
que no necesita devolver ningún valor (lo muestra directamente). Aunque el resultado ob-
tenido es el mismo, esta nueva forma de implementar la función media resta flexibilidad.
Por ejemplo, ahora no serı́a posible lo siguiente:

1 int main() {
2 int num1, num2, num3;
3

4 printf("Introduce tres valores: ");


5 scanf(" %d %d %d", &num1, &num2, &num3);
6 if( media(num1, num2, num3) > 5 ) // Sentencia incorrecta
7 printf("APROBADO\n");
8 else
9 printf("SUSPENSO\n");
10 return 0;
11 }

70
4.3 Creando nuestras propias funciones

Ahora la función media no devuelve ningún resultado (es de tipo void), por lo que no
es posible comparar si el resultado de la misma es mayor o menor que cinco. En este
ejemplo nuestro objetivo no era mostrar por pantalla la media obtenida, sino operar con
el resultado obtenido, por lo tanto nos interesaba una función media que devolviese un
valor numérico, tal y como hicimos en el primer ejemplo.

De forma similar, tampoco resulta interesante hacer la lectura de datos dentro de la propia
función media:

1 float media() {
2 int a, b, c, res;
3 printf("Introduce tres valores: ");
4 scanf(" %d %d %d", &a, &b, &c);
5 res = (a+b+c)/3.0;
6 return res;
7 }

En este caso es la propia función media la que se encarga de solicitar la introducción


de datos. Obsérvese que ahora la función carece de parámetros, ya que no es necesario
transferir ningún valor en la llamada. Nuevamente este esquema no resulta interesante ya
que podrı́a darse alguna situación en la que no se desee calcular la media de tres valores
introducidos por el usuario, sino de algún otro valor resultante de ciertos cálculos. Por
ejemplo, la función anterior no podrı́a utilizarse en la siguiente situación:

1 int main() {
2 int num1, num2, num3, num4;
3 float m;
4

5 printf("Introduce cuatro valores: ");


6 scanf(" %d %d %d %d", &num1, &num2, &num3, &num4);
7 m = media(num1, num2, num3*num4);
8 . . .
9 }

En definitiva, salvo en aquellos casos en los que el acometido de la función sea explı́cita-
mente solicitar o mostrar datos, será conveniente que las operaciones de entrada/salida se
realicen fuera de la función que realiza los cálculos. De este modo las funciones imple-
mentadas podrán utilizarse en diferentes contextos, esto es, estaremos creando funciones
más generales.

71
Capı́tulo 4. Funciones

4.3.5 Prototipos de funciones y ficheros de cabecera


Como se ha dicho ya anteriormente, un programa en C está formado por un conjunto
de funciones. Es importante que cada una de estas funciones esté definida antes de su
llamada. En los ejemplos anteriores puede observarse que las funciones fact o media
son llamadas desde main. Si observamos dichos ejemplos, puede comprobarse que en
todos los casos estas funciones se definen antes de la función main y, por tanto, antes de
que sean llamadas.

Sin embargo, existe una alternativa que permite implementar las funciones en cualquier
orden, pudiendo incluso estar la implementación después de la llamada. Para ello basta
con declarar la función antes de la llamada (habitualmente al inicio del fichero). La
declaración de una función consiste en escribir el prototipo, esto es, la cabecera, seguido
de un punto y coma. Recordemos que la cabecera de una función está formada por el tipo
de dato devuelto, el nombre y la lista de parámetros:

tipo_dato nombre_funcion(lista_parametros);

Por ejemplo, el prototipo de la función fact del ejemplo dado en la sección anterior serı́a:

1 int fact(int num);

En consecuencia,
 haciendo uso de la declaración de funciones, en el ejemplo del cálculo
de m n serı́a posible implementar las funciones en el siguiente orden:

1 #include <stdio.h>
2

3 int fact(int num); // Declaración de la


función fact
4

5 int main() {
6 . . .
7 res = fact(m) / (fact(n)*fact(m-n)); // Llamadas a la función
fact
8 . . .
9 }
10

11 int fact( int num ) { // Implementación de la


función fact
12 . . .
13 }

Como se observa, las llamadas a fact preceden a la implementación, lo cual es posible


por haber declarado la función con anterioridad.
72
4.4 Variables locales y globales

En la declaración de la función se puede omitir el nombre de los parámetros. En realidad


el compilador lo único que necesita conocer de antemano es el nombre de la función, el
tipo de dato que devuelve y el número y tipo de parámetros; el nombre de los mismos
es irrelevante. Con esta información el compilador puede comprobar si la llamada a la
función es correcta (coinciden el número y tipo de argumentos con el número y tipo de
parámetros), sin necesidad de conocer la implementación. En consecuencia, la declara-
ción de la función fact podrı́a ser simplemente:

1 int fact(int);

Cuando utilizamos funciones de la librerı́a de C es necesario declararlas antes de su utili-


zación, ya que en este caso la implementación de las mismas no se encuentra en nuestro
fichero fuente (en realidad ni siquiera se dispone de la implementación sino del código
ya compilado o código objeto). Por ejemplo, cualquier programa que utilice la función
printf deberı́a declararla previamente. Para facilitar esta labor existen unos ficheros es-
peciales que contienen las declaraciones (cabeceras) de las funciones. Estos ficheros se
denominan ficheros de cabecera o header files. Por poner sólo algunos ejemplos, el fiche-
ro stdio.h contiene las cabeceras o declaraciones de todas las funciones relacionadas
con la entrada/salida estándar (printf, scanf, etc.) y el fichero math.h contiene las
cabeceras de las funciones matemáticas(pow, sqrt, sin, cos, ...). En consecuencia, para
declarar estas funciones basta con incluir el fichero de cabecera correspondiente mediante
una instrucción #include.

1 #include <stdio.h>
2 #include <math.h>
3 . . .

La extensión .h viene precisamente de header file, si bien estos ficheros además de ca-
beceras de funciones también pueden contener otra información como declaración de
constantes (mediante instrucciones #define) o declaración de tipos estructurados, algo
que se verá en el Capı́tulo 6.

4.4 Variables locales y globales


Se define el ámbito de una variable como la porción de programa donde esa variable es
visible y, por tanto, puede accederse a su contenido. En este sentido las variables pueden
clasificarse en variables locales (cuando el ámbito se restringe a una función) y variables
globales (cuando el ámbito se extiende a todo el programa).

Variables locales: Una variable es local cuando se declara dentro de una función, bien en
el cuerpo de la misma, bien como parámetro. Por ejemplo, dado el siguiente programa:

73
Capı́tulo 4. Funciones

1 int fact( int n ) {


2 int i, f=1;
3 for( i=2; i<=n; i++ )
4 f = f * i;
5 return f;
6 }
7 int main() {
8 int num, res;
9 . . .
10 res = fact(num);
11 . . .
12 }

las variables n, i y f son locales a la función fact y por tanto no es posible usarlas fuera
de esta función. De modo similar las variables num y res son locales a la función main.
En realidad, todas las variables que hemos venido usando hasta ahora en los ejemplos
mostrados son variables locales.

Es posible que dos variables que se encuentren en funciones distintas tengan el mismo
nombre. Por ejemplo, el siguiente código serı́a perfectamente válido:

1 int fact( int n ) {


2 int i, f=1;
3 for( i=2; i<=n; i++ )
4 f = f * i;
5 return f;
6 }
7 int main() {
8 int i, res;
9 . . .
10 res = fact(i);
11 . . .
12 }

En este caso tanto la función fact como main contienen una variable de nombre i. Esto
no supone ningún problema, de hecho es algo bastante habitual, pero debe quedar claro
que la variable i de la función fact es distinta de la variable i de main. Cada una ocupa
su propio espacio de memoria y no hay confusión posible entre ellas, ya que dependiendo
de la función donde se estén utilizando quedará claro a qué variable nos referimos.

En el ejemplo mostrado la variable i de main representa el valor del cual queremos calcu-
lar el factorial, mientras que la variable i de fact se usa a modo de contador en el bucle
for. El hecho de que la variable i de fact vaya cambiando su valor en cada iteración del
bucle no afecta para nada a la variable i de main, la cual mantiene su valor.
74
4.4 Variables locales y globales

Variables globales: Una variable es global cuando se declara fuera de cualquier función
(normalmente a continuación de las instrucciones #include y #define). El ámbito de
una variable global es la totalidad del programa y, por tanto, puede ser utilizada en cual-
quier parte del mismo.

El uso de variables globales evita tener que pasar información a las funciones mediante
el uso de parámetros, o devolverla mediante el uso de return, sin embargo, tal y como
veremos más adelante, su uso no se recomienda.

En el siguiente programa las variable num y f se declaran a nivel global:

1 #include <stdio.h>
2 int num, f; // Variables globales
3 void fact() {
4 int i; // Variable local
5 f = 1;
6 for( i=2; i<=num; i++ )
7 f = f * i;
8 }
9 int main() {
10 printf("Introduce un número entero positivo: ";
11 scanf(" %d", &num);
12 fact();
13 printf("FACTORIAL( %d)= %d\n", num, f);
14 }

En el ejemplo mostrado las variables num y f declaradas en la lı́nea 2 son globales y, por
tanto, conocidas tanto dentro de main como de fact. Obsérvese que ahora la función
fact no necesita recibir ni devolver ningún valor. La función main puede dar valor a la
variable num (lı́nea 11) y, posteriormente, la función fact consultar el valor de esa misma
variable (lı́nea 6).

Esta forma de trabajar está totalmente desaconsejada, ya que rompe en gran medida con
el principio de modularidad. Es interesante que las funciones tengan una alta cohesión
(la función realiza una única tarea bien definida) y un bajo acoplamiento (la función
tiene una baja dependencia de otras funciones). El uso de variables globales aumenta la
dependencia entre funciones, esto es, aumenta el acoplamiento. Otros motivos por los que
debe evitarse el uso de variables globales son:

Código más difı́cil de comprender.


Errores imprevistos debidos a efectos colaterales: esto sucede cuando se altera de
forma no deseada el contenido de una variable en algún punto del programa. Si
dicha variable es global, puede resultar muy complicado localizar el error, mientras

75
Capı́tulo 4. Funciones

que si la variable se restringe al ámbito de una función (variable local) el error


será más fácil de localizar.

4.5 Paso de argumentos por valor y por referencia


En la Sección 4.3 se ha visto el modo en que se transfiere la información de una función
a otra mediante el uso de argumentos. Existen dos formas de pasar los argumentos: por
valor y por referencia.

4.5.1 Paso de argumentos por valor


Tal y como se ha explicado con anterioridad, cuando se transfiere información de una
función a otra mediante el uso de argumentos, se copia el valor que tiene cada uno de
los argumentos de la llamada al parámetro correspondiente de la función invocada. Por
ejemplo, dado el siguiente código:

1 void f(int v) {
2 . . .
3 }
4

5 int main() {
6 int num;
7 . . .
8 f(num);
9 . . .
10 }

la llamada f(num) provoca que se copie el valor contenido en num en el parámetro v


definido en f.

Esto es lo que se conoce como pase de parámetros por valor. Hay que tener en cuenta
que las variables num y v ocupan espacios de memoria distintos (incluso aunque ambas
tuviesen el mismo nombre) por lo que un cambio en el valor de v no afecta para nada a la
variable num. Veamos el siguiente ejemplo:

1 #include <stdio.h>
2

3 void modificar( int a ) {


4 printf("Valor de ’a’ antes de modificar: %d\n", a);
5 a = a + 2;
6 printf("Valor de ’a’ después de modificar: %d\n", a);
7 }
8

76
4.5 Paso de argumentos por valor y por referencia

9 int main() {
10 int a = 3;
11 printf("Valor de ’a’ al inicio de main: %d\n", a);
12 modificar(a);
13 printf("Valor de ’a’ al final de main: %d\n", a);
14 return 0;
15 }

El resultado de ejecutar este programa es:

Valor de ’a’ al inicio de main: 3


Valor de ’a’ antes de modificar: 3
Valor de ’a’ después de modificar: 5
Valor de ’a’ al final de main: 3

Como puede observarse, el valor de la variable a declarada en main no se altera aunque


modifiquemos el valor de la variable a declarada en modificar.

En definitiva, mediante el pase de argumentos por valor, no es posible que una función
modifique el valor de una variable perteneciente a otra función. Esta forma de trabajar
es la más adecuada en muchas situaciones, pero en ocasiones interesa que una función
modifique alguna de las variables de la función que la invocó. Esto se consigue mediante
el paso de argumentos por referencia, el cual se basa en el uso de punteros. Para poder
entender el siguiente apartado es necesario haber comprendido previamente el manejo de
punteros explicado en el Capı́tulo 2.

4.5.2 Paso de argumentos por referencia


El paso de argumentos por referencia consiste en pasar la dirección de una variable en
lugar de su valor. Esta forma de pasar los argumentos permite que una función pueda mo-
dificar las variables pertenecientes a otra, tal y como se muestra en el siguiente ejemplo:

1 void modificar( int * p ) {


2 *p = *p + 2;
3 }
4

5 int main() {
6 int a = 3;
7 printf("Valor de ’a’ al inicio de main: %d\n", a);
8 modificar(&a);
9 printf("Valor de ’a’ al final de main: %d\n", a);
10 return 0;
11 }

77
Capı́tulo 4. Funciones

En este ejemplo, la llamada modificar(&a) pasa la variable a por referencia, esto es,
pasamos la dirección de a en lugar de su valor. Esta llamada produce que en el parámetro p
de la función modificar se copie la dirección de a. A partir de este momento, la función
modificar podrá acceder a la variable a ya que conoce su dirección de memoria. En
concreto, la operación *p = *p + 2 incrementa en 2 el valor de a.

Imaginemos una situación en la que una función necesita devolver dos valores. Ya se ha
visto que mediante la sentencia return únicamente es posible devolver un valor2 , por lo
que deberı́amos hacer uso del pase de parámetros por referencia para transferir los valores
deseados. En el siguiente ejemplo se muestra el uso de una función que, dado el radio de
una circunferencia, devuelve (mediante argumentos pasados por referencia), tanto el área
como el perı́metro.

1 #include <stdio.h>
2 #define PI 3.141592
3 void area_perim(float radio, float * p_area, float * p_perim);
4

5 int main() {
6 float r, area, perim;
7

8 printf("Introduce radio: ");


9 scanf(" %f", &r);
10 area_perim(r, &area, &perim);
11 printf("AREA= %f PERIMETRO= %f\n", area, perim);
12 return 0;
13 }
14

15 void area_perim(float radio, float * p_area, float * p_perim) {


16 *p_area = PI * radio * radio;
17 *p_perim = 2 * PI * radio;
18 }

En este ejemplo, la llamada area_perim(r, &area, &perim) de la lı́nea 10 provoca


que el valor de la variable r se copie en el parámetro radio de la función area_perim y
que las direcciones de memoria de las variables area y perim se copien en los paráme-
tros p_area y p_perim respectivamente. En consecuencia, cuando dentro de la función
area_perim empleamos *p_area o *p_perim (lı́neas 16-17), estaremos accediendo en
realidad a la zona de memoria asignada a las variables area y perim respectivamente.

2 En realidad es posible devolver un grupo de variables mediante el uso de estructuras, tal y como se verá en

el Capı́tulo 6, pero si nos restringimos a los tipos simples únicamente podremos devolver un valor.
78
4.6 Ejercicios resueltos

4.6 Ejercicios resueltos

4.6.1 Funciones que no devuelven ningún valor


Escribir un programa que dibuje por pantalla la siguiente figura:

*
**
***
*****
******
*******
********
*********
**********

Para ello implementar una función que reciba un número entero n y muestre por
pantalla una lı́nea con n asteriscos.
SOLUCIÓN:

1 #include <stdio.h>
2

3 void dibuja_asteriscos(int);
4

5 int main() {
6 int i;
7 for(i=1; i<=10; i++) {
8 dibuja_asteriscos(i);
9 }
10 return 0;
11 }
12

13 void dibuja_asteriscos( int n ) {


14 int i;
15 for(i=1; i<=n; i++) {
16 printf("*");
17 }
18 printf("\n");
19 }

En la lı́nea 3 se encuentra la declaración de la función. Esta lı́nea indica que en


algún punto de nuestro programa debe existir una función de nombre dibuja_asteriscos
que recibe como parámetro un entero (int) y no devuelve ningún valor (void).

79
Capı́tulo 4. Funciones

Entre las lı́neas 13 y 19 se encuentra la implementación de la función. La función


recibe un entero que almacena en la variable n y, mediante un bucle, muestra por
pantalla n asteriscos seguidos de un salto de lı́nea (\n).
En la lı́nea 8 se encuentra la llamada a la función. Puesto que la función espera
recibir un valor entero, en la llamada debemos proveer dicho valor (en este caso el
valor de la variable i). En el momento de la llamada, el valor de i se copia en la
variable n.
No debe confundirse la variable i de la función main con la variable de igual
nombre de la función dibuja_asteriscos. Aunque tengan el mismo nombre, se
trata de variables completamente distintas.
Puesto que la función es de tipo void (no devuelve ningún valor) la llamada no
puede formar parte de ninguna expresión. Por ejemplo no tendrı́a sentido una ins-
trucción como:

1 x = dibuja_asteriscos(i);

4.6.2 Funciones que devuelven un valor


Implementar un programa que calcule el área de un anillo de radios r1, r2 (area =
πr22 − πr12 ). El programa deberá solicitar, tantas veces como sea necesario, los
valores de r1 y r2 para asegurarse de que ambos son mayores de cero y de que
r2 > r1. A continuación, mediante el uso de una función, deberá calcular el área.
Finalmente mostrará el resultado. Tanto la petición de datos como la salida de re-
sultados deberá realizarse desde el programa principal.
SOLUCIÓN:

1 #include <stdio.h>
2 #include <math.h>
3

4 float area_anillo(float, float);


5

6 int main() {
7 float r1, r2, area;
8 do {
9 printf("Introduce los radios del anillo: ");
10 scanf(" %f %f", &r1, &r2);
11 } while( r1<=0 || r2<=0 || r2<=r1 );
12

13 area = area_anillo(r1, r2);


14 printf("AREA= %f\n", area);
15 return 0;
16 }
80
4.6 Ejercicios resueltos

17

18 float area_anillo( float rad1, float rad2 ) {


19 float res;
20 res = PI*rad2*rad2 - PI*rad1*rad1;
21 return res;
22 }

En la lı́nea 2 se incluye la librerı́a math.h para poder utilizar la constante PI.


En la lı́nea 4 se declara la función area_anillo. Se especifica que la función
recibe dos números reales (float) como parámetros y devuelve otro real.
Entre las lı́neas 8 y 11 se realiza la petición de datos. El bucle do-while provoca
que los datos se vuelvan a solicitar en caso de que sean incorrectos.
En la lı́nea 13 se realiza la llamada a la función. En el momento de la llamada los
valores de r1 y r2 se copian en las variables rad1 y rad2.
Entre las lı́nea 18 y 21 se implementa la función. El valor calculado se devuelve
mediante la sentencia return.
Puesto que la función es de tipo float (devuelve un valor real) la llamada puede
formar parte de cualquier expresión que requiera un número real. Por ejemplo, las
siguientes instrucciones serı́an válidas:
• area = area_anillo(r1, r2);
• area_cuadrado = pow(area_anillo(r1,r2), 2);
• printf("El área es %f\n", area_anillo(r1,r2));
• if( area_anillo(r1,r2) < 1.5 )

4.6.3 Funciones que devuelven un valor del tipo VERDADERO/FALSO


Realizar un programa que muestre todos los números primos comprendidos entre
1 y 100. Para ello, implementar una función que devuelva si un número dado es
primo.
SOLUCIÓN:

1 #include <stdio.h>
2

3 int primo(int);
4

5 int main() {
6 int i;
7

8 for(i=1; i<=100; i++) {


9 if( primo(i)==1 )
10 printf(" %d\n", i);
81
Capı́tulo 4. Funciones

11 }
12 return 0;
13 }
14

15 int primo( int n ) {


16 int i, es_primo;
17

18 es_primo = 1;
19 for(i=2; i<n; i++) {
20 if( n %i == 0 )
21 es_primo = 0;
22 }
23 return es_primo;
24 }

En la lı́nea 3 se declara la función. Las funciones que devuelven un valor del tipo
VERDADERO/FALSO se declaran de tipo int, de modo que si la función da como
resultado VERDADERO se devuelve 1 y si da FALSO se devuelve 0. Esta función
además de devolver un entero, recibe como parámetro otro entero (el número que
deseamos evaluar si es o no primo).
En la lı́nea 9 se realiza la llamada a la función. El valor de i se copia en la varia-
ble n. Cuando finaliza la función, esta llamada se sustituye por el valor devuelto
en la instrucción de la lı́nea 23. Si se devuelve 1, la instrucción de la lı́nea 9 rea-
lizará la comparación if(1==1), mientras que si devuelve 0 realizará la compara-
ción if(0==1).
Entre las lı́neas 15 y 24 se implementa la función. La variable es_primo se usa
a modo de flag o bandera. Se inicializa con 1 y, si durante la ejecución del bucle
for se encuentra algún valor i que sea un divisor exacto de n (n%i == 0) enton-
ces se cambia el flag a 0 ya que, con toda seguridad, el número n no será primo.
Finalmente en la lı́nea 23 se devuelve el valor del flag.
Una posible mejora a este algoritmo consistirı́a en iterar únicamente hasta n/2en el
bucle for de la lı́nea 19, ya que no vamos a encontrar divisores más grandes que
este valor. Por otro lado, también podrı́a forzarse la salida del bucle en cuanto se
encuentre un divisor, en cuyo caso queda claro que el número no es primo y por
tanto no sirve de nada seguir iterando en el bucle.

4.6.4 Funciones que devuelven más de un valor


Escribir un programa que solicite un tiempo dado en segundos y calcule, mediante
una función, la cantidad de horas, minutos y segundos que corresponden al tiempo
introducido.

82
4.6 Ejercicios resueltos

SOLUCIÓN:

1 #include <stdio.h>
2

3 void convertir(int , int *, int *, int *);


4

5 int main() {
6 int seg_total, hor, min, seg;
7

8 printf("Introduce tiempo en segundos: ");


9 scanf(" %d", &seg_total);
10 convertir(seg_total, &hor, &min, &seg);
11 printf(" %ds = %dh %dm %ds\n", seg_total, hor, min, seg);
12

13 return 0;
14 }
15

16 void convertir(int st, int * h, int * m, int * s ) {


17 *h = st/3600;
18 *m = (st %3600)/60;
19 *s = st %60;
20 }

Una función no puede devolver más de un valor a través de una sentencia return
(a no ser que lo haga mediante una variable de tipo struct, las cuales todavı́a no
se han estudiado). Un modo habitual de devolver más de un valor es mediante el
pase de parámetros por referencia.
En la lı́nea 10 se encuentra la llamada a la función convertir. Esta función tiene
cuatro parámetros: los segundos totales (pasado por valor) y las direcciones de me-
moria de las variables hor, min y seg (pase por referencia) declaradas en main.
Es necesario pasar estas tres últimas variables por referencia, ya que su valor debe
ser modificado dentro de la función. En el momento de la llamada, en el parámetro
st (lı́nea 16) se copia el valor de seg_total, y en los parámetros h, m y s se
copian las direcciones de memoria de las variables hor, min y seg respectiva-
mente. Esto es, en el momento de la llamada se produce la siguiente transferencia
de información: st=seg_total, h=&hor, m=&min y s=&seg.
Entre las lı́neas 16 y 20 se encuentra la implementación de la función. Esta función
asigna valores a las variables hor y min y seg declaradas en main a través de los
punteros h, m y s.
En la lı́nea 3 se encuentra la declaración de la función. Puede observarse que los
tres últimos parámetros son de tipo puntero, ya que corresponden, no al valor de
una variable, sino a su dirección de memoria. La función es de tipo void ya que no
devuelve nada a través de la sentencia return.

83
Capı́tulo 4. Funciones

4.7 Ejercicios propuestos


1. Implementar una función que, dado un número entero positivo, muestre por pantalla
todos sus divisores. Escribir un programa que solicite un número y, mediante una
llamada a la función anterior, muestre sus divisores.

2. Implementar una función que, dado un valor de temperatura expresado en grados


centı́grados, devuelva el equivalente expresado en grados farenheit (F=C*9/5+32).
Escribir un programa que, mediante llamadas a la función anterior, muestre la
equivalencia entre grados centı́grados y farenheit para todos los valores de grados
centı́grados comprendidos entre 0 y 30 con incrementos de dos en dos.

3. Implementar una función que calcule el módulo de un número complejo (dado


mediante su parte real e imaginaria). Escribir un programa que solicite un número
complejo y muestre su módulo.
4. Implementar una función que reciba tres valores (los dos primeros reales y el tercero
entero) y devuelva:
La suma de los dos primeros si el tercero es igual a 1.
La resta de los dos primeros si el tercero es igual a 2.
El producto de los dos primeros si el tercero es igual a 3.
La división de los dos primeros si el tercero es igual a 4.
Cero en cualquier otro caso

Escribir un programa que pida dos números y una operación (1-4) y muestre el
resultado en función de la operación solicitada.

5. Implementar una función que, dados tres números reales, devuelva el mayor de
ellos. Escribir un programa que haga uso de esta función para mostrar el mayor de
tres valores introducidos por teclado.

6. Implementar una función que, dados tres números reales, devuelva si están orde-
nados crecientemente. Escribir un programa que solicite tres números y muestre si
están o no ordenados.
7. Implementar una función que devuelva si dos números dados son amigos. Dos
números a y b son amigos si a es la suma de los divisores de b y b es la suma
de los divisores de a, sin considerar la división por si mismo. Por ejemplo 220 y
284 son números amigos, ya que:
Los divisores de 220 (sin contar él mismo) son 1, 2, 4, 5, 10, 11, 20, 22, 44,
55 y 110, que suman 284.
Los divisores de 284 son 1, 2, 4, 71 y 142, que suman 220.

84
4.7 Ejercicios propuestos

Otros pares de números amigos son: (1184, 1210), (6232, 6368), (17296, 18416) y
(9363584, 9437056).
Escribir un programa que solicite dos números enteros positivos y muestre si son o
no amigos.

8. Implementar una función que, dados dos números reales que representen el ángu-
lo (a) y módulo (m) de un vector, devuelva las coordenadas cartesianas de dicho
vector (x = m cos(a), y = m sin(a)). Escribir un programa que solicite un vector
expresado en coordenadas polares y lo muestre en coordenadas cartesianas.

9. Implementar una función que, dados dos números reales, los intercambie en el caso
de que el primero sea mayor que el segundo. Escribir un programa que solicite dos
valores y, tras invocar a la función anterior, los muestre ordenados.

85
Capı́tulo 5

Vectores

5.1 Introducción
Es posible distinguir entre tipos de datos básicos y tipos compuestos. Las variables de
tipos básicos (char, int, float, double y tipo puntero) pueden almacenar un solo
dato en un instante determinado. Por contra, las variables de tipos compuestos permiten
almacenar un conjunto de valores.

Un vector en C es una variable compuesta que permite almacenar una serie de valores,
todos ellos del mismo tipo. Gráficamente, podemos representar un vector tal y como se
muestra en la Figura 5.1.

Denominamos:

Elemento: cada una de las variables individuales que componen el vector.

Talla o rango: número de elementos que contiene el vector.

0 1 2 3 4 5 6 7 Í
Índices

V 1 1 2 3 5 8 13 21

Elementos
E del vector
Nombre del vector

Figura 5.1: Representación gráfica de un vector

87
Capı́tulo 5. Vectores

Índice: la posición que ocupa cada elemento dentro del vector. Al primer elemento
se le asocia el ı́ndice 0 y al último el ı́ndice talla-1.

La Figura 5.1 representa un vector de 8 elementos. El primer elemento, en la posición 0,


almacena el valor 1, mientras que el último elemento, en la posición 7, almacena el valor
21.

5.2 Declaración de vectores


Para declarar un vector se emplea la siguiente sintaxis:

tipo_dato nombre_vector[talla];

donde tipo_dato especifica el tipo de dato de cada uno de los elementos que forman el
vector y talla el número de elementos. Por ejemplo, la sentencia

1 int v[10];

declara un vector de nombre v que contiene 10 elementos de tipo int.

Al igual que ocurre con otros tipos de variables, es posible inicializar los valores de un
vector en el momento de la declaración. Por ejemplo, la siguiente sentencia crea un vector
de enteros de nombre dias_mes e inicializa cada uno de los elementos con los dı́as que
tiene cada uno de los meses del año:

1 int dias_mes[12] = {31,28,31,30,31,30,31,31,30,31,30,31};

Vemos que para inicializar un vector deben escribirse los valores de cada uno de sus
elementos encerrados entre llaves y separados por coma. La sentencia anterior crea el
vector dias_mes y almacena el valor 31 en la posición 0, el 28 en la posición 1, etc.

Cuando se inicializa el vector en el momento de la declaración, es posible omitir el ta-


maño. En este caso se crea un vector del tamaño necesario para almacenar todos los
valores especificados. Por ejemplo, la siguiente sentencia crea un vector de tamaño 4 e
inicializa cada uno de sus elementos con los valores especificados entre llaves.

1 float temperaturas[] = {17.5, 18.6, 20.1, 22.5};

88
5.3 Acceso a los elementos de un vector

5.3 Acceso a los elementos de un vector


Para acceder a los distintos elementos de un vector se emplea la siguiente sintaxis:

nombre_vector[indice]

donde indice hace referencia al elemento al que deseamos acceder. Debe recordarse que
el primer elemento tiene ı́ndice 0 y el último ı́ndice talla-1.

Por ejemplo, dado el vector

1 int v[10];

las siguientes sentencias almacenan un 5 en la primera posición y un 20 en la última:

1 v[0] = 5;
2 v[9] = 20;

En general, dado un vector

tipo_dato nombre_vector[talla];

la expresión nombre_vector[indice] se comporta, a todos los efectos, como una va-


riable de tipo tipo_dato, por lo que puede ser empleada bajo las mismas condiciones
que se emplearı́a cualquier otra variable de ese tipo. Por ejemplo, dado el siguiente pro-
grama:

1 #include <stdio.h>
2

3 int main() {
4 int a, b, c;
5 printf("Introduce dos números enteros: ");
6 scanf(" %d %d", &a, &b);
7 c = a + b;
8 printf("La suma de %d y %d es %d\n", a, b, c);
9 return 0;
10 }

se podrı́a reescribir usando vectores del siguiente modo:

89
Capı́tulo 5. Vectores

1 #include <stdio.h>
2

3 int main() {
4 int v[3];
5 printf("Introduce dos números enteros: ");
6 scanf(" %d %d", &v[0], &v[1]);
7 v[2] = v[0] + v[1];
8 printf("La suma de %d y %d es %d\n", v[0], v[1], v[2]);
9 return 0;
10 }

Obsérvese que v[0], v[1] y v[2] se usan exactamente igual a como se usarı́a cualquier
otra variable de tipo int (en este caso, igual a como se usan las variables a, b y c del
primer ejemplo).

5.4 Operaciones con vectores: automatización mediante bucles


Cualquier operación que se desee realizar con un vector, deberá hacerse elemento a ele-
mento. El lenguaje C no permite operar con el vector en su totalidad. Por ejemplo, si
queremos sumar dos vectores v1, v2 y almacenar el resultado en v3, debemos sumar
uno a uno cada uno de los elementos de los vectores. La sentencia v3 = v1 + v2 es
incorrecta (más adelante veremos cómo hacer ésta operación).

Imaginemos que disponemos de un vector de 10 elementos enteros y deseamos dar valores


a cada uno de ellos. El siguiente programa permite hacer esta operación, aunque de un
modo poco recomendable.

1 #include <stdio.h>
2

3 int main() {
4 int v[10];
5

6 printf("Introduce el valor del primer elemento: ");


7 scanf(" %d", &v[0]);
8 printf("Introduce el valor del segundo elemento: ");
9 scanf(" %d", &v[1]);
10 printf("Introduce el valor del tercer elemento: ");
11 scanf(" %d", &v[2]);
12

13 . . .
14

15 printf("Introduce el valor del décimo elemento: ");


16 scanf(" %d", &v[9]);

90
5.4 Operaciones con vectores: automatización mediante bucles

17

18 . . .
19 return 0;
20 }

Este modo de trabajar, además de poco operativo, es impracticable cuando el tamaño del
vector es grande. Sin embargo, resulta sencillo acceder secuencialmente a los elementos
de un vector mediante el uso de bucles. Para ello debe usarse una variable entera en el
lugar del ı́ndice, de modo que ésta vaya variando en cada iteración del bucle, tal y como
se muestra en el siguiente ejemplo:

1 #include <stdio.h>
2

3 int main() {
4 int i, v[10];
5

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


7 printf("Introduce el valor del elemento %d: ", i);
8 scanf(" %d", &v[i]);
9 }
10 . . .
11 return 0;
12 }

Obsérvese que en el bucle del programa anterior, la variable i comienza con valor 0 y
finaliza con valor 9, por lo que en cada iteración la sentencia scanf va almacenando
valores en los distintos elementos del vector (v[0], v[1], v[2], ...). Debe recordarse
que en un vector v de tamaño 10, sus elementos van desde v[0] hasta v[9].

Habitualmente el tamaño de un vector se define con una constante simbólica mediante la


directriz #define. Ello permite modificar fácilmente el tamaño del vector sin necesidad
de tener que cambiar la condición en todos los bucles de acceso al vector. A continuación
se muestra un programa de ejemplo que permite sumar dos vectores, dejando el resultado
en un tercero:

1 #include <stdio.h>
2 #define N 10
3

4 int main() {
5 int i, v1[N], v2[N], v3[N]; // Vectores de tamaño 10
6

7 // Introducimos los datos del primer vector


8 printf("Valores del primer vector:\n");
9 for( i=0; i<N; i++) {

91
Capı́tulo 5. Vectores

10 printf("Introduce el valor del elemento %d: ", i);


11 scanf(" %d", &v1[i]);
12 }
13

14 // Introducimos los datos del segundo vector


15 printf("Valores del segundo vector:\n");
16 for( i=0; i<N; i++) {
17 printf("Introduce el valor del elemento %d: ", i);
18 scanf(" %d", &v2[i]);
19 }
20

21 // Realizar la suma v3 = v1 + v2 elemento a elemento


22 for( i=0; i<N; i++)
23 v3[i] = v1[i] + v2[i];
24

25 // Mostrar el resultado de la suma


26 printf("Vector suma:\n");
27 for( i=0; i<N; i++)
28 printf(" %d ", v3[i]);
29

30 return 0;
31 }

Obsérvese que los bucles for de las lı́neas 9, 16, 22 y 27 iteran con el ı́ndice i desde 0 a
N-1, siendo N el tamaño de los vectores. De este modo, si se quisiera trabajar con vectores
de otro tamaño, bastarı́a con cambiar el valor de N definido en la lı́nea 2. Obsérvese tam-
bién que para sumar dos vectores, debemos hacerlo elemento a elemento (lı́neas 22-23).

5.5 Paso de vectores a una función


Al igual que ocurre con otro tipo de variables, los vectores también pueden ser pasados
a funciones, aunque, tal y como se explicará más adelante, el mecanismo de pase de
parámetros en este caso es sensiblemente distinto.

A continuación se muestra la sintaxis a utilizar cuando se pasan vectores como parámetro


a una función. Para ello distinguimos entre la llamada a la función, la implementación y
la declaración.

92
5.5 Paso de vectores a una función

Llamada a la función:

nombre_funcion(nombre_vector)

Por ejemplo

1 inicializar(v);

Implementación de la función:

tipo nombre_funcion( tipo nombre_vector[talla] ) {


// Cuerpo de la función
}

donde talla se puede omitir, en cuyo caso la función admite vectores de cualquier ta-
maño.

Por ejemplo

1 void inicializar( int v[] ) {


2 int i;
3 for( i=0; i<N; i++ ) // Suponiendo un vector de tamaño N
4 v[i] = 0;
5 }

Declaración de la función:

tipo nombre_funcion( tipo nombre_vector[talla] );

donde tanto nombre_vector como talla se pueden omitir.

Por ejemplo

1 void inicializar( int [] );

Visto en conjunto dentro de un programa, las sentencias anteriores quedarı́an como sigue:

93
Capı́tulo 5. Vectores

1 #include <stdio.h>
2 #define N 10
3

4 // Declaración de la función
5 void inicializar( int [] );
6

7 // Programa principal
8 int main() {
9 int vect[N];
10 inicializar(vec); // Llamada a la función
11 . . .
12 return 0;
13 }
14

15 // Implementación de la función
16 void inicializar( int v[] ) {
17 int i;
18 for( i=0; i<N; i++ ) // Suponiendo un vector de tamaño N
19 v[i] = 0;
20 }

A continuación se muestra un sencillo ejemplo de un programa que solicita los valores de


un vector y seguidamente los muestra por pantalla.

1 #include <stdio.h>
2 #define N 10
3

4 // Declaración de funciones
5 void pedir_valores( int [] );
6 void mostrar_valores( int [] );
7

8 // Programa principal
9 int main() {
10 int vec[N];
11

12 pedir_valores(vec); // Llamada a función


13 mostrar_valores(vec);
14

15 return 0;
16 }
17

18 // Implementación de las funciones


19 void pedir_valores( int v[] ) {
20 int i;
21 for( i=0; i<N; i++) {

94
5.5 Paso de vectores a una función

22 printf("Introduce el valor del elemento %d: ", i);


23 scanf(" %d", &v[i]);
24 }
25 }
26

27 void mostrar_valores( int v[] ) {


28 int i;
29 for( i=0; i<N; i++)
30 printf(" %d ", v[i]);
31 }

En el ejemplo anterior, la llamada pedir_valores de la lı́nea 12 pasa el vector vec a la


función correspondiente, implementada en la lı́nea 19, la cual rellena el vector a través de
la función scanf. Es importante entender que tras la ejecución de esta función, el vector
vec declarado en main mantiene los valores dados dentro de la función. Este comporta-
miento difiere de lo visto en el pase de parámetros por valor explicado en la Sección 4.5.1
del Capı́tulo 4, en el que se explicó que las modificaciones hechas dentro de una función
a los parámetros pasados por valor, no afectan a la variable original enviada. Lo que ocu-
rre en este caso es que los vectores se pasan siempre por referencia, sin tener que usar
explı́citamente la sintaxis de punteros. En consecuencia, siempre que se pase un vector
como parámetro a una función, cualquier alteración que se haga sobre el vector dentro
de la función quedará reflejada en el vector original enviado. En el ejemplo anterior, en
realidad el vector vec declarado en main y el vector v utilizado dentro de las funciones
hacen referencia al mismo espacio de memoria y, por tanto, cuando se modifique v, real-
mente se estará modificando vec. Este comportamiento se explica con mayor detalle en
la Sección 5.10.

Obviamente, una misma función puede ser invocada con distintos vectores en distintos
puntos del programa, lo que permite la reutilización de la función. En el siguiente ejemplo
se muestra un programa que suma dos vectores mediante el uso de funciones. Obsérvese
cómo se invoca más de una vez a una misma función, con distintos vectores en cada caso.

1 #include <stdio.h>
2 #define N 10
3

4 // Declaración de funciones
5 void pedir_valores( int [] );
6 void mostrar_valores( int [] );
7 void sumar( int [], int [], int [] );
8

9 // Programa principal
10 int main() {
11 int v1[N], v2[N], v3[N];
12

13 pedir_valores(v1);
95
Capı́tulo 5. Vectores

14 pedir_valores(v2);
15 sumar(v1, v2, v3);
16 printf("Vector 1:\n");
17 mostrar_valores(v1);
18 printf("Vector 2:\n");
19 mostrar_valores(v2);
20 printf("Suma:\n");
21 mostrar_valores(v3);
22

23 return 0;
24 }
25

26 // Implementación de las funciones


27 void pedir_valores( int v[] ) {
28 int i;
29 for( i=0; i<N; i++) {
30 printf("Introduce el valor del elemento %d: ", i);
31 scanf(" %d", &v[i]);
32 }
33 }
34

35 void mostrar_valores( int v[] ) {


36 int i;
37 for( i=0; i<N; i++)
38 printf(" %d ", v[i]);
39 printf("\n");
40 }
41

42 void sumar( int x[], int y[], int z[] ) {


43 int i;
44 for( i=0; i<N; i++)
45 z[i] = x[i] + y[i];
46 }

Observar en el ejemplo anterior que las funciones pedir_valores y mostrar_valores


se invocan con un vector distinto en cada ocasión. En la implementación de las funcio-
nes, el vector v recibido como parámetro hace referencia al argumento pasado en cada
momento (v1, v2 o v3). También debe resaltarse el modo en que se ha implementado
la función sumar. Debe observarse que esta función no devuelve el vector resultante (la
función es de tipo void) sino que dicho vector se pasa como un argumento más. Esto es
posible ya que, tal y como se ha dicho anteriormente, los vectores se pasan por referencia
y, por tanto, es posible modificar su contenido dentro de la función.

Por último, cabe señalar que en ocasiones resulta interesante diseñar funciones que sean
capaces de operar con vectores de distintos tamaños (las funciones de los ejemplos an-
teriores, por el modo en que están implementados los bucles for, están pensadas única-
96
5.5 Paso de vectores a una función

mente para vectores de tamaño N). Si se desea operar con vectores de distintos tamaños,
es necesario pasar como parámetro tanto el vector como su tamaño, tal y como se muestra
en el siguiente ejemplo, que calcula la media de distintos vectores:

1 #include <stdio.h>
2 #define N 10
3 #define M 15
4

5 // Declaración de funciones
6 void pedir_valores( float [], int );
7 float media( float [], int );
8

9 // Programa principal
10 int main() {
11 float v1[N], v2[M], m1, m2;
12

13 pedir_valores(v1, N);
14 pedir_valores(v2, M);
15 m1 = media(v1, N);
16 m2 = media(v2, M);
17 printf("Media de v1: %f\n", m1);
18 printf("Media de v2: %f\n", m2);
19

20 return 0;
21 }
22

23 // Implementación de las funciones


24 void pedir_valores( float v[], int dim ) {
25 int i;
26 for( i=0; i<dim; i++) {
27 printf("Introduce el valor del elemento %d: ", i);
28 scanf(" %d", &v[i]);
29 }
30 }
31

32 float media( float v[], int dim ) {


33 int i;
34 float suma = 0;
35 for( i=0; i<dim; i++)
36 suma += v[i];
37 if( dim == 0 )
38 return 0;
39 else
40 return suma/dim;
41 }

97
Capı́tulo 5. Vectores

Obsérvese que, en este caso, los bucles for implementados en las funciones (lı́neas 26
y 35) tienen como lı́mite la variable dim recibida como parámetro, la cual podrá tener
valores distintos dependiendo de la llamada (lı́neas 13-16).

Además de los ejemplos mostrados a lo largo de esta sección, existen operaciones tı́picas
con vectores que, debido a su uso habitual en programas de ı́ndole muy diversa, conviene
conocer en detalle, como por ejemplo la obtención del máximo, del mı́nimo, búsqueda de
un elemento, etc. En el apartado de ejercicios resueltos (Sección 5.12) se puede encontrar
la solución de éstos y otros algoritmos.

5.6 Devolución de vectores en una función


Las funciones en C no pueden devolver vectores, aunque sı́ pueden devolver un puntero
a un vector. En este caso esta operación únicamente serı́a conveniente cuando el vector
haya sido reservado de forma dinámica. En este libro no se aborda la reserva dinámica de
memoria y, por tanto, en ningún caso emplearemos funciones que devuelvan un puntero
a un vector.

Si el resultado de una función es un vector, éste lo declararemos en main y lo pasaremos


como un argumento más, tal y como hemos hecho en las funciones pedir_datos y
sumar de los ejemplos anteriores.

5.7 Vectores multidimensionales


El lenguaje C también permite trabajar con vectores de más de una dimensión (matrices).
En este capı́tulo abordaremos únicamente las matrices de dos dimensiones. Una matriz
bidimensional es un conjunto de variables de un mismo tipo organizado en filas y colum-
nas.

Para declarar una matiz bidimensional se emplea la siguiente sintaxis:

tipo_dato nombre_matriz[talla_filas][talla_columnas];

donde tipo_dato especifica el tipo de dato de cada uno de los elementos de la matriz
y talla_fila y talla_columnas indican el número de filas y columnas de la matriz.
Por ejemplo, la sentencia

1 int m[3][4];

declara una matriz m de enteros con 3 filas y 4 columnas.

98
5.8 Operaciones con matrices: automatización mediante bucles

0 1 2 3

0 0,0 0,1 0,2 0,3

1 1,0 1,1 1,2 1,3

2 2,0 2,1 2,2 2,3

Figura 5.2: Índices en una matriz de 3 x 4.

Al igual que ocurre con los vectores, los ı́ndices comienzan en 0 y terminan en talla - 1.
En la Figura 5.2 se muestra los ı́ndices (fila, columna) de cada uno de los elementos de la
matriz anterior.

Para acceder a los elementos de una matriz se emplea la siguiente sintaxis:

nombre_matriz[indice_fila][indice_columna];

Por ejemplo, dada la matriz

1 int m[3][4];

las siguientes sentencias almacenan un 5 en el primer elemento de la matriz (primera fila,


primera columna) y un 20 en el último elemento (última fila, última columna):

1 m[0][0] = 5;
2 m[2][3] = 20;

5.8 Operaciones con matrices: automatización mediante


bucles
Al igual que ocurre con los vectores, las operaciones con matrices deben realizarse ele-
mento a elemento. Para ello es conveniente (y con matrices mı́nimamente grandes, abso-
lutamente necesario) automatizar el acceso a los elementos de la matriz mediante el uso
de bucles.

Para recorrer una matriz bidimensional emplearemos dos variables enteras que vayan to-
mando los valores de sus ı́ndices (fila y columna). Esto se consigue mediante el uso de
dos bucles anidados (ver la Sección 3.2.4 para entender el funcionamiento de los bucles
anidados).
99
Capı́tulo 5. Vectores

En el siguiente ejemplo se declara una matriz de 3 filas y 4 columnas y, a continuación,


se solicitan los valores de cada uno de sus elementos:

1 #include <stdio.h>
2 #define FIL 3
3 #define COL 4
4

5 int main() {
6 int i, j, m[FIL][COL];
7

8 for( i=0; i<FIL; i++ ) {


9 for( j=0; j<COL; j++ ) {
10 printf("Introduce valor del elemento ( %d, %d): ", i, j);
11 scanf(" %d", &m[i][j]);
12 }
13 }
14 . . .
15 return 0;
16 }

Es importante entender cómo van variando los ı́ndices i, j tras cada iteración de los
bucles. En la primera iteración del bucle más externo (lı́nea 8) se inicializa la variable i a
0 y, seguidamente, se entra en el bucle. A continuación se ejecutan todas las iteraciones del
bucle interno (lı́neas 9-12) en las que j varı́a de 0 a COL-1, esto es, j toma sucesivamente
los valores 0, 1, 2 y 3 con i valiendo 0. Tras la finalización de todas las iteraciones
del bucle interno, se vuelve a la siguiente iteración del bucle externo, con la variable i
incrementada en 1. A continuación se ejecutan de nuevo todas las iteraciones del bucle
interno, donde j toma nuevamente los valores 0, 1, 2 y 3, pero esta vez con i valiendo 1.
De este modo se procede hasta haber recorrido todos los elementos de la matriz.

Para mostrar por pantalla una matriz m de enteros de tamaño FIL x COL utilizarı́amos el
siguiente código:

1 for( i=0; i<FIL; i++ ) {


2 for( j=0; j<COL; j++ ) {
3 printf(" %d\t" m[i][j]);
4 }
5 printf("\n");
6 }

Obsérvese que la sentencia printf de la lı́nea 5 provoca que, tras mostrarse todas las
columnas de una fila dada (esto es, tras haberse completado el bucle interno), se inserte
un salto de lı́nea, con lo que la siguiente fila se mostrará en una lı́nea distinta.

100
5.9 Paso de matrices a una función

5.9 Paso de matrices a una función


El paso de matrices a una función es similar, en cuanto a sintaxis y funcionamiento, al
paso de vectores. A continuación se muestra la sintaxis empleada:

Llamada a la función:

nombre_funcion(nombre_matriz)

Implementación de la función:

tipo nombre_funcion( tipo nombre_matriz[talla_filas][talla_columnas] ) {


// Cuerpo de la función
}

donde talla_filas se puede omitir, en cuyo caso la función admite matrices con cual-
quier número de filas, pero talla_columnas debe especificarse obligatoriamente.

Declaración de la función:

tipo nombre_funcion( tipo nombre_matriz[talla_filas][talla_columnas]);

donde tanto nombre_matriz como talla_filas se pueden omitir, si bien talla_columnas


debe especificarse obligatoriamente.

Como se observa, la principal diferencia respecto de los vectores radica en que en este
caso hay que utilizar una doble pareja de corchetes ( [ ][ ] ) en lugar de una simple ( [ ] ).
Hay que recordar que, como en el caso de los vectores, con las matrices también se emplea
el método de paso de argumentos por referencia.

En el siguiente ejemplo se solicitan los valores de dos matrices de tamaño 3 x 5, se


suman, y se muestra por pantalla la matriz resultante:

1 #include <stdio.h>
2 #define N 3
3 #define M 5
4

5 // Declaración de funciones
6 void pedir_valores( int [][M] );
7 void mostrar_valores( int [][M] );
8 void sumar( int [][M], int [][M], int [][M] );
9

10 // Programa principal
11 int main() {
101
Capı́tulo 5. Vectores

12 int m1[N][M], m2[N][M], m3[N][M];


13

14 pedir_valores(m1);
15 pedir_valores(m2);
16 sumar(m1, m2, m3);
17 printf("Matriz 1:\n");
18 mostrar_valores(m1);
19 printf("Matriz 2:\n");
20 mostrar_valores(m2);
21 printf("Suma:\n");
22 mostrar_valores(m3);
23

24 return 0;
25 }
26

27 // Implementación de las funciones


28 void pedir_valores( int m[][M] ) {
29 int i, j;
30 for( i=0; i<N; i++) {
31 for( j=0; j<M; j++) {
32 printf("Introduce valor del elemento ( %d, %d): ", i, j);
33 scanf(" %d", &m[i][j]);
34 }
35 }
36

37 void mostrar_valores( int m[][M] ) {


38 int i, j;
39 for( i=0; i<N; i++) {
40 for( j=0; j<M; j++) {
41 printf(" %d\t" m[i][j]);
42 }
43 printf("\n");
44 }
45 }
46

47 void sumar( int x[][M], int y[][M], int z[][M] ) {


48 int i, j;
49 for( i=0; i<N; i++)
50 for( j=0; j<M; j++)
51 z[i][j] = x[i][j] + y[i][j];
52 }

102
5.10 Relación entre vectores y punteros

5.10 Relación entre vectores y punteros


Ya se explicó en la Sección 2.9 que todas las variables tienen asociada una dirección de
memoria, que indica la posición que ocupa dicha variable en la memoria del ordenador.
En el caso de los vectores habrı́a que precisar que, dado que éstos son, en realidad, un
conjunto de variables (a los que denominamos elementos del vector), cada una de estos
elementos tiene asociada una dirección.

Hay que entender que los elementos de un vector se almacenan de forma consecutiva
en memoria, por lo tanto, basta con conocer la dirección del primer elemento para saber
también la del resto. Esto es, dado el siguiente fragmento de código

1 int v[10];
2 int * p;
3 p = &v[0]

en el que se observa que el puntero p almacena la dirección de memoria del primer ele-
mento del vector, entonces sabremos que el segundo elemento está en la dirección p+1,
el tercero en p+2 y ası́ sucesivamente. Vemos por tanto que, a partir del puntero p, es po-
sible acceder a todos los elementos del vector. Para ello debemos recordar que mediante
el operador de indirección * se puede acceder a la variable apuntada por un puntero. En
consecuencia, el fragmento de código

1 int v[10];
2 v[0] = 0;
3 v[1] = 1;
4 v[2] = 2;

es equivalente a1

1 int v[10];
2 int * p;
3 p = &v[0];
4 *p = 0;
5 *(p+1) = 1;
6 *(p+2) = 2;

1 Conviene aclarar que cada uno de los elementos de un vector puede ocupar más de un byte, dependiendo del

tipo. Por ejemplo, los enteros ocupan 4 bytes. Esto quiere decir que cada elemento del vector ocupa en realidad
4 posiciones (direcciones) de memoria. Debe entenderse que cuando se emplea aritmética de punteros, dado un
puntero p, la operación p+n en realidad hace p+(n*tamaño-del-dato). Es por ello que en la declaración de un
puntero, resulta necesario indicar el tipo de dato al cual apunta.

103
Capı́tulo 5. Vectores

Por otro lado hay que saber que el nombre de un vector, utilizado sin corchetes, representa
en realidad la dirección de memoria de su primer elemento. Esto es, dado

1 int v[10];

v es, en realidad, &v[0].

En consecuencia, el fragmento de código anterior también podrı́a haberse escrito del si-
guiente modo:

1 int v[10];
2 *v = 0;
3 *(v+1) = 1;
4 *(v+2) = 2;

En resumen, dada la declaración:

1 int v[10];

y entendiendo que v es, en realidad, la dirección del primer elemento del vector, entonces
las expresiones v[i] y *(v+i) son equivalentes. Esto es, podemos acceder a los ele-
mentos de un vector mediante la sintaxis de corchetes que hemos venido utilizando hasta
ahora, o mediante la sintaxis de punteros que acabamos de presentar.

Por ejemplo, dado el vector int v[10], el siguiente bucle inicializa todos los elementos
del vector a cero:

1 for( i=0; i<10; i++ )


2 *(v+i) = 0; // Equivale a v[i] = 0;

Por último hay que entender que, cuando en los ejemplos de la Sección 5.5 empleábamos
la sentencia

1 inicializar(v);

en realidad lo que se está ejecutando es

1 inicializar(&v[0]);

104
5.11 Cadenas de caracteres

lo que explica que los vectores siempre se pasan por referencia, aunque para ello no
utilicemos explı́citamente la sintaxis clásica de punteros.

La implementación de la función inicializar con la sintaxis empleada hasta el mo-


mento

1 void inicializar( int v[] ) {


2 int i;
3 for( i=0; i<N; i++ ) // Suponiendo un vector de tamaño N
4 v[i] = 0;
5 }

puede reescribirse como

1 void inicializar( int * v ) {


2 int i;
3 for( i=0; i<N; i++ ) // Suponiendo un vector de tamaño N
4 *(v+i) = 0;
5 }

o incluso alternando ambos tipos de sintaxis:

1 void inicializar( int * v ) {


2 int i;
3 for( i=0; i<N; i++ ) // Suponiendo un vector de tamaño N
4 v[i] = 0;
5 }

5.11 Cadenas de caracteres


Cuando se necesita almacenar información en forma de texto en una variable (nombres,
apellidos, direcciones o, en general, cualquier cadena de texto) empleamos una cadena
de caracteres. Una cadena de caracteres (también llamada string) es un vector de tipo
char que almacena una determinada secuencia de caracteres (letras, dı́gitos, signos de
puntuación...) seguida del carácter especial ’\0’. Este carácter especial se emplea para
indicar el fin de la cadena, de modo que lo que sigue a ’\0’ en el vector es, por lo general,
ignorado, por lo que el contenido de estos elementos es irrelevante. La Figura 5.3 muestra
la cadena “HOLA” almacenada en un vector de tipo char.

Cuando trabajamos con vectores de caracteres, es muy habitual que desconozcamos a


priori el tamaño de la cadena que se va a almacenar, por lo que será necesario declarar
estos vectores suficientemente grandes como para dar cabida a la cadena más larga que
105
Capı́tulo 5. Vectores

0 1 2 3 4 5 6 7 8 9

H O L A \0 - - - - -

Figura 5.3: Ejemplo de una cadena en C.

queramos permitir, más un elemento extra para el carácter ’\0’. Por ejemplo, si estuviése-
mos pensando en una aplicación que almacene mensajes de twitter, deberı́amos utilizar
vectores de tamaño 141, ya que un tweet tiene una longitud máxima de 140 caracteres.

El motivo de usar el carácter especial ’\0’ es el de permitir almacenar cadenas de menor


longitud al tamaño del vector. Si no tuviésemos este carácter especial, serı́a imposible
determinar qué caracteres del vector forman parte de la cadena y cuáles no. Como hemos
dicho anteriormente, ’\0’ indica ”fin de cadena”. Debe entenderse que una variable de tipo
char siempre contiene un valor (un carácter) aunque no haya sido inicializada (al igual
que una variable int siempre va a contener un entero). No es posible almacenar algo
que indique ”no_valor”, de ahı́ la necesidad de indicar de algún modo dónde termina la
cadena. En el ejemplo de la figura anterior, los elementos del 5 al 9, en caso de no haber
sido inicializados, contendrán cualquier carácter arbitrario.

Las cadenas de caracteres, por sus peculiaridades, tienen un tratamiento especial en C. La


principal diferencia con otro tipo de vectores es que, en este caso, no es necesario operar
elemento a elemento, sino que la librerı́a de C dispone de funciones que permiten realizar
ciertas operaciones con la cadena completa. En los siguientes apartados de explican las
principales operaciones con cadenas de caracteres.

5.11.1 Entrada y salida con cadenas de caracteres


En este apartado estudiaremos las funciones printf y puts empleadas para mostrar
cadenas de caracteres por al salida estándar (esto es, por pantalla) y las funciones scanf y
gets empleadas para almacenar cadenas introducidas por la entrada estándar (el teclado)
en un vector de caracteres.

Función printf:

Dado el siguiente vector de caracteres

1 char cad[50];

en el que supondremos almacenada una cadena con su correspondiente ’\0’ al final de la


misma, la función printf permite mostrar su contenido por pantalla del siguiente modo:

1 printf(" %s", cad);


106
5.11 Cadenas de caracteres

Como vemos, el especificador “%s” indica que la variable asociada es una cadena de
caracteres (string). En este caso, la función printf mostrará por pantalla los caracteres
almacenados en cad, desde la posición 0 del vector hasta la aparición de ’\0’ (sin incluir
este último).

El resultado del código anterior serı́a idéntico al siguiente:

1 i = 0;
2 while( cad[i] != ’\0’ ) {
3 printf(" %c", cad[i]);
4 i++;
5 }

En este caso, en lugar de mostrar toda la cadena de golpe con el especificador ”%s”, lo
vamos haciendo caracter a caracter con el especificador ”%c”. Obsérvese que, en el primer
caso, a la función printf se le pasa como argumento el vector completo (cad), mientras
que en este segundo ejemplo se le pasa, en cada iteración, un único elemento del vector
(cad[i]), que corresponde a una variable de tipo char. Obviamente, el primer método
resulta mucho más cómodo.

Por supuesto, al igual que ocurre con otras variables, la función printf puede mostrar,
junto con la cadena, otra información. Por ejemplo:

1 printf("La cadena cad contiene el texto %s\n", cad);

Función puts:

La función puts (acrónimo de put string) se emplea también para mostrar cadenas de
caracteres por pantalla. Su sintaxis es:

puts(nombre_cadena);

La principal diferencia con printf radica en que puts no admite otros argumentos extra
y en que inserta automáticamente un salto de lı́nea (\n). La instrucción

1 puts(cad);

es equivalente a

1 printf(" %s\n", cad);

107
Capı́tulo 5. Vectores

Función scanf:

Para almacenar en una cadena de caracteres un texto introducido por teclado se puede
utilizar la sentencia scanf con el especificador ”%s”, tal y como muestra el siguiente
fragmento de código:

1 char cad[50];
2 printf("Introduce un texto: ");
3 scanf(" %s", cad);

El código anterior provoca que el texto introducido por teclado se almacene en el vector
cad. Además, el carácter especial ’\0’ es introducido automáticamente al final del texto.

Debe prestarse especial atención a que, en este caso, en la sentencia scanf no se utiliza
el operador & precediendo al nombre de la variable, tal y como venı́amos haciendo hasta
ahora. La explicación de ello está relacionada con lo explicado en la Sección 5.10, en la
que se vio que el nombre de un vector (cad en este caso) es, en realidad, la dirección de
memoria de su primer elemento (&cad[0]). Es decir, sin necesidad de poner el operador
& delante del nombre de la cadena, ésta ya se está pasando por referencia.

La función scanf lee caracteres hasta que se encuentre un salto de lı́nea (provocado me-
diante la pulsación de la tecla enter), un espacio en blanco o un tabulador. Esto implica
que scanf no permite la lectura de cadenas que contengan espacios en blanco. Por ejem-
plo, si tras la ejecución de la sentencia scanf("%s", cad) el usuario introduce por
teclado el texto "Esto es una cadena" seguido de la pulsación de la tecla enter, en
la cadena cad únicamente se almacenará el texto "Esto". Para la lectura de cadenas que
contengan espacios en blanco deberá emplearse la función gets.

Función gets:

Esta función lee caracteres de la entrada estándar (el teclado) hasta que encuentra un salto
de lı́nea y los almacena en una cadena. El salto de lı́nea (\n) no se almacena en la cadena.
A continuación se muestra un ejemplo de uso:

1 char cad[50];
2 printf("Introduce un texto: ");
3 gets(cad);

108
5.11 Cadenas de caracteres

5.11.2 Funciones de manipulación de cadenas de caracteres


La librerı́a de C dispone de diversas funciones para el manejo de cadenas de caracteres.
Todas ellas están definidas en el fichero de cabecera string.h, por lo que para utilizarlas
en un programa deberı́a incluirse dicho fichero mediante la siguiente instrucción:

1 #include <string.h>

A continuación se muestran algunas de las funciones más usadas. En este caso usaremos
la notación de punteros (ver Sección 5.10), ya que es de este modo como se encuen-
tra habitualmente en la bibliografı́a. Debe entenderse que char * str es equivalente a
char str[].

Función Descripción
int strlen(char *str) Devuelve la longitud de la cadena str,
sin incluir el carácter de terminación
’\0’.
char * strcpy(char *c1, char *c2) Copia la cadena almacenada en c2 a la
cadena c1, incluyendo el carácter de ter-
minación ’\0’. Devuelve un puntero a la
cadena destino (la misma que se pasa co-
mo parámetro, por lo que este valor de-
vuelto es redundante).
char * strcat(char *c1, char *c2) Concatena la cadena almacenada en c2
a continuación la cadena almacenada en
c1. El carácter de terminación ’\0’ de
la cadena c1 se sobreescribe con el pri-
mer carácter de la cadena c2 y se añade
un nuevo ’\0’ en c1, al final de la con-
catenación de las dos cadenas. Devuelve
un puntero a la cadena destino (la misma
que se pasa como parámetro, por lo que
este valor devuelto es redundante).
int strcmp(char *c1, char *c2) Compara las cadenas c1 y c2. Devuel-
ve cero si ambas cadenas son iguales, un
valor positivo si c1 es, en orden alfabéti-
co, posterior a c2, o un valor negativo si
c1 es, en orden alfabético, anterior a c2.

109
Capı́tulo 5. Vectores

A continuación se muestra un ejemplo de uso:

1 #include <stdio.h>
2 #include <string.h>
3 #define N 50
4

5 int main() {
6 char cad1[N], cad2[N], cad3[N];
7 int x;
8

9 strcpy(cad1, "Hola"); // Copia en cad 1 el texto Hola


10 printf(" %s tiene %d caracteres\n", cad1, strlen(cad1));
11 strcpy(cad2, cad1); // Copia en cad2 el contenido de cad1
12 strcat(cad2, "Adios"); // Concatena "Adios" a cad2
13 printf(" %s\n", cad2);
14 strcpy(cad3, "Mundo");
15 x = strcmp(cad1, cad3);
16 if( x == 0 )
17 printf(" %s y %s son iguales\n", cad1, cad3);
18 else if( x > 0 )
19 printf(" %s es alfabéticamente posterior a %s\n", cad1, cad3
);
20 else
21 printf(" %s es alfabéticamente anterior a %s\n", cad1, cad3)
;
22

23 return 0;
24 }

El resultado de la ejecución del programa anterior es el siguiente:

Hola tiene 4 caracteres


HolaAdios
Hola es alfabéticamente anterior a Mundo

5.12 Ejercicios resueltos


1. Implementar un programa que solicite 50 valores reales, los almacene en un vector,
y obtenga el valor máximo.

110
5.12 Ejercicios resueltos

SOLUCIÓN:

1 #include <stdio.h>
2 #define N 50
3

4 void pedir_valores( float v[] );


5 float maximo( float v[] );
6

7 int main() {
8 float v[N], m;
9

10 pedir_valores(v);
11 m = maximo(v);
12 printf("Máximo: %f\n", m);
13

14 return 0;
15 }
16

17 void pedir_valores( float v[] ) {


18 int i;
19 for( i=0; i<N; i++ ) {
20 printf("Introduce elemento %d: ", i);
21 scanf(" %f", &v[i]);
22 }
23 }
24

25 float maximo( float v[] ) {


26 int i;
27 float max;
28 max = v[0];
29 for( i=1; i<N; i++ ) {
30 if( v[i] > max )
31 max = v[i];
32 }
33 return max;
34 }

Como puede observarse, para obtener el máximo de un vector inicializamos una


variable (max en el ejemplo anterior) con el primer elemento del vector (lı́nea 28). A
continuación se itera sobre el resto de elementos del vector y, si se encuentra algún
valor que supere lo que hasta el momento tenemos en max, se actualiza el valor de
esta variable (lı́neas 30-31). La búsqueda del mı́nimo serı́a similar, cambiando la
comparación > por <.
2. Implementar un programa que almacene 50 valores enteros en un vector, a conti-
nuación solicite un nuevo valor, y diga si dicho valor se encuentra en el vector.

111
Capı́tulo 5. Vectores

SOLUCIÓN:

1 #include <stdio.h>
2 #define N 50
3

4 void pedir_valores( int v[] );


5 int pertenece( int v[], int x );
6

7 int main() {
8 int v[N], num;
9

10 pedir_valores(v);
11 printf("Introduce el valor a buscar: ");
12 scanf(" %d", &num);
13 if( pertenece(v, num) )
14 printf(" %d se encuentra en el vector\n", num);
15 else
16 printf(" %d no se encuentra en el vector\n", num);
17

18 return 0;
19 }
20

21 void pedir_valores( float v[] ) {


22 int i;
23 for( i=0; i<N; i++ ) {
24 printf("Introduce elemento %d: ", i);
25 scanf(" %f", &v[i]);
26 }
27 }
28

29 int pertenece( int v[], int x ) {


30 int i = 0;
31 while( i < N && v[i] != x )
32 i++;
33 return (i<N); // Devuelve 1 si i<N y 0 en caso contrario
34 }

Obsérvese que en la función pertenece del ejercicio anterior el bucle de la lı́nea


31 finaliza, bien cuando se encuentra el valor buscado, en cuyo caso el valor de la
variable i será necesariamente menor que N, bien cuando se ha recorrido el vector
completo sin que se haya encontrado el valor buscado, en cuyo caso la variable i
valdrá N.

3. Implementar una función que, dada una matriz de enteros de tamaño NxM, obtenga
su traspuesta.

112
5.12 Ejercicios resueltos

SOLUCIÓN:

1 void traspuesta( int m1[N][M], m2[M][N] ) {


2 int i, j;
3 for( i=0; i<N; i++ )
4 for( j=0; j<M; j++ )
5 m2[j][i] = m1[i][j];
6 }

4. Implementar una función que realice la misma operación que la función strlen
de la librerı́a de C (cálculo de la longitud de una cadena).
SOLUCIÓN:

1 int mi_strlen( char cad[] ) {


2 int i = 0;
3 while( cad[i] != ’\0’ )
4 i++;
5 return i;
6 }

5. Implementar una función que, dada una cadena de caracteres que contiene palabras
separadas por un espacio en blanco, muestre la última palabra de la cadena.
SOLUCIÓN:

1 void ultima_palabra( char cad[] ) {


2 int i = 0;
3

4 // Nos situamos al final de la cadena


5 while( cad[i] != ’\0’ )
6 i++;
7

8 // Retrocedemos hasta encontrar un espacio en blanco,


9 // o hasta el inicio en caso de que no haya espacios
10 while( i > 0 && cad[i] != ’ ’ )
11 i--;
12

13 // Mostramos los caracteres hasta el final


14 if( cad[i] == ’ ’ )
15 i++;
16 while( cad[i] != ’\0’ ) {
17 printf(" %c", cad[i]);
18 i++;
19 }
20 }

113
Capı́tulo 5. Vectores

5.13 Ejercicios propuestos


1. Escribir un programa que solicite la introducción de 20 números y muestre:
Los valores que superan la media
El porcentaje de valores que supera la media

2. Implementar una función que, dados un vector y un valor, devuelva la posición que
ocupa dicho valor en el vector (o -1 si el valor no se encuentra).

3. Implementar una función que, dado un vector de reales, muestre por pantalla todos
los máximos locales. Un elemento de un vector es un máximo local cuando su valor
es mayor que el anterior y el posterior. Consideraremos que el primer y último
elemento del vector no son, en ningún caso, máximos locales.

4. Escribir un programa que solicite una cadena de caracteres y nos diga si es palı́ndro-
ma (se lee igual del derecho que del revés).

5. Dados dos vectores V 1 y V 2 de tamaños N y M respectivamente, cuyos elementos


se encuentran ordenados de menor a mayor, implementar una función que almacene
en un tercer vector V 3 de tamaño N + M los elementos de V 1 y V 2, de modo que
los elementos de V 3 queden también ordenados de menor a mayor.

6. Escribir un programa que permita jugar al Master Mind. El mecanismo de este


juego es el siguiente: el ordenador, de forma aleatoria, selecciona 4 números dis-
tintos, cada uno de ellos comprendido entre 1 y 8. El jugador tiene que averiguar
los números que ha seleccionado el ordenador y en el orden correcto. Para ello el
jugador irá haciendo intentos, indicando 4 números. Tras cada intento del jugador,
el ordenador le responderá la cantidad de “números rojos” (número acertado y en
la posición correcta) y la cantidad de “números verdes” (número acertado, pero
no en la posición correcta). Por ejemplo, suponiendo que el ordenador ha escogido
aleatoriamente el número 4263, lo siguiente podrı́a ser el desarrollo de una partida:

1234 1 Rojo, 2 Verdes


1325 0 Rojos, 2 Verdes
4632 1 Rojo, 3 Verdes
4263 4 Rojos

Nota.- Para generar un número aleatorio entre 1 y 8, utilizar la instrucción rand()%8+1.

7. Implementar una función que, dada una matriz cuadrada, calcule su traspuesta
(cambiar filas por columnas). La función recibirá una única matriz, y el resulta-
do se almacenará sobre la matriz original.

8. Se dispone de una matriz S de M filas por N columnas, en la que cada fila almacena
los valores en voltios de cierta señal eléctrica a lo largo del tiempo. La matriz S
114
5.13 Ejercicios propuestos

almacena, por tanto, los valores de M señales eléctricas muestreadas cada una de
ellas en N instantes de tiempo, representando el elemento S[i][j] el valor en voltios
de la señal i en el instante j. Por otro lado se dispone de un vector V de N elementos
que almacena igualmente los valores en voltios de una señal eléctrica muestreada
en N instantes de tiempo. Se desea comparar la señal almacenada en el vector V
con cada una de las M señales almacenadas en la matriz S y obtener aquella señal
de S que más se parece a la almacenada en V . Para calcular cómo de “parecidas”
son dos señales X e Y se utiliza la fórmula:
N
 −1
similitud = (Xi − Yi )2
i=0

Cuanto menor sea este valor, más parecidas serán las señales X e Y . Se pide im-
plementar las funciones necesarias que nos permitan obtener, a partir de la matriz
S y el vector V , el ı́ndice de la fila en la matriz S donde se encuentra la señal más
parecida a V .

115
Capı́tulo 6

Estructuras

6.1 Introducción
Cuando ciertas variables están fuertemente relacionadas entre sı́, resulta interesante agru-
parlas bajo una misma entidad. Una estructura es un agrupamiento de variables en el que
éstas pueden ser de distinto tipo (a diferencia de los vectores, en los que todos sus elemen-
tos son del mismo tipo). A los elementos de una estructura se los denomina miembros.

6.2 Declaración de variables de tipos estructurados


Hasta ahora se han visto los tipos de datos simples int, float, char, double y el tipo
puntero. El lenguaje C permite crear nuevos tipos de datos para definir agrupaciones de
variables de distinta naturaleza. Este es el tipo struct. A diferencia de los tipos básicos,
que ya están definidos en el lenguaje y pueden ser utilizados para declarar variables, el
tipo struct debe crearse a medida, con los miembros que nos interese en cada caso. Una
vez creado el tipo, será posible declarar variables del mismo, de manera similar a como
se declaran variables de cualquier otro tipo.

En resumen, para declarar variables de un tipo estructurado se requieren dos pasos:

Definir un tipo de dato estructurado (se definen los miembros que va a tener
nuestro agrupamiento).

Declarar variables de dicho tipo.

117
Capı́tulo 6. Estructuras

6.2.1 Definición de tipos estructurados


Para definir un tipo estructurado se debe dar un nombre al nuevo tipo e indicar el conjunto
de elementos (miembros) que contendrá.

struct nombre_tipo_datos {
tipo_dato miembro1;
tipo_dato miembro2;
. . .
};

Por ejemplo:

1 struct persona {
2 char nombre[50];
3 float altura;
4 float peso;
5 int anyo_nacimiento;
6 };

En este ejemplo se define un nuevo tipo de datos de nombre struct persona que esta-
blece un agrupamiento de cuatro miembros (nombre, altura, peso y anyo_nacimiento).
Debe quedar claro que el nombre del nuevo tipo es la palabra compuesta struct persona
y no simplemente persona.

Por otro lado es importante entender que de momento no hemos declarado ninguna varia-
ble. Simplemente hemos definido un nuevo tipo de datos. En este ejemplo struct persona
es equivalente a int o float en el sentido de que es un tipo de dato. La diferencia
principal es que struct persona es un tipo compuesto (esto es, una variable de tipo
struct persona podrá almacenar un conjunto de valores) mientras que int, por poner
un ejemplo, es untipo básico (una variable de tipo int sólo podrá almacenar un valor).
Una vez definido el tipo, será posible declarar variables del mismo.

6.2.2 Declaración de variables


Una vez definido el tipo de datos, las variables de un tipo estructurado se declaran como
cualquier otra variable:

tipo nombre_variable;

Por ejemplo, la sentencia

1 struct persona p;

118
6.3 Operaciones con estructuras

crea una variable p de tipo struct persona. Dicha variable p es una variable com-
puesta, ya que internamente almacena varios datos. Concretamente la variable p contiene
cuatro miembros: nombre, altura, peso y anyo_nacimiento. De hecho, cuando se
declara la variable p, se reserva memoria para almacenar 50 caracteres (nombre), 2 floats
(peso y altura) y 1 entero (año de nacimiento).

6.3 Operaciones con estructuras

6.3.1 Inicialización
Al igual que ocurre con otros tipos de variables, las variables de tipo estructurado se
pueden inicializar en el momento de la declaración. Para ello se encierran entre llaves y se
separan por comas los valores con los que se desea inicializar cada uno de los miembros.

1 struct persona p = {"W. Amadeus Mozart", 1.52, 47, 1756};

6.3.2 Acceso a los miembros de una estructura


Para acceder a los miembros de una estructura se utiliza la siguiente sintaxis:

nombre_variable.miembro

El par nombre_variable.miembro se utiliza de modo similar a como se usarı́a cual-


quier otra variable. Por ejemplo, dada la variable p de tipo struct persona lo siguiente
serı́an instrucciones válidas:

1 p.peso=60;
2 p.altura=1.75;
3 printf(" %s", p.nombre);
4 scanf(" %d", &p.anyo_nacimiento);

A continuación se muestra un ejemplo completo:

1 #include <stdio.h>
2

3 // Definimos el tipo de dato ’struct persona’


4 struct persona {
5 char nombre[50];
6 float altura;
7 float peso;
119
Capı́tulo 6. Estructuras

8 int anyo_nacimiento;
9 };
10

11 int main() {
12 struct persona p; // Declaramos una variable de tipo ’struct
persona’
13 float imc;
14

15 printf("Introduce tu nombre: ");


16 scanf(" %s", p.nombre);
17 printf("Introduce tu altura (en metros): ");
18 scanf(" %f", &p.altura);
19 printf("Introduce tu peso (en kg.): ");
20 scanf(" %f", &p.peso);
21 printf("Introduce tu año de nacimiento: ");
22 scanf(" %d", &p.anyo_nacimiento);
23

24 imc = p.peso/(p.altura*p.altura);
25 printf("Hola %s ", p.nombre);
26 printf("Tu Indice de Masa Corporal es %f\n", imc);
27 return 0;
28 }

Obsérvese que los tipos estructurados suelen definirse a nivel global, fuera de la función
main. De este modo será posible declarar variables del tipo estructurado no solo en main
sino en cualquier otra función de nuestro programa.

6.3.3 Asignación
A diferencia de lo que ocurre con los vectores, entre los que no se permite la asignación,
entre variables de tipo estructurado sı́ que es posible realizar esta operación.

Por ejemplo, la operación

1 struct persona p1, p2;


2 . . .
3 p1 = p2;

es válida y serı́a equivalente a:

1 struct persona p1, p2;


2 . . .
3 strcpy(p1.nombre, p2.nombre);
4 p1.altura = p2.altura;
120
6.3 Operaciones con estructuras

5 p1.peso = p2.peso;
6 p1.anyo_nacimiento = p2.anyo_nacimiento;

Es curioso observar que aunque no es posible copiar todos los elementos de un vector
en otro mediante la operación de asignación (debe hacerse elemento a elemento median-
te un bucle o, en el caso de las cadenas, mediante la función strcpy), si dicho vector se
encuentra dentro de una estructura, entonces la asignación sı́ es posible. En el ejemplo an-
terior, cuando se hace la asignación p1 = p2 se está copiando (entre otras cosas) el vector
p2.nombre en p1.nombre. Sin embargo, hacer directamente p1.nombre = p2.nombre
no serı́a válido ya que, como se ha mencionado, la asignación entre vectores no está per-
mitida.

6.3.4 Otras operaciones


En general, salvo la asignación, el resto de operaciones con estructuras (lectura, escritura,
comparación, etc.) hay que hacerlas miembro a miembro.

Por ejemplo, el siguiente código serı́a incorrecto si lo que pretendemos es comparar el


contenido de dos estructuras:

1 struct persona p1, p2;


2 . . .
3 if( p1 == p2 ) {
4 printf("Ambas personas tienen el mismo nombre, altura, peso y
año nac.");

La forma correcta de hacer esta comparación serı́a miembro a miembro:

1 struct persona p1, p2;


2 . . .
3 if( strcmp(p1.nombre, p2.nombre)==0 &&
4 p1.altura==p2.altura && p1.peso==p2.peso &&
5 p1.anyo_nacimiento==p2.anyo_nacimiento)
6 printf("Ambas personas tienen el mismo nombre, altura, peso y
año nac.");

121
Capı́tulo 6. Estructuras

6.4 Estructuras anidadas


Los miembros de una estructura pueden ser de cualquier tipo de datos, incluido otras
estructuras. Por ejemplo:

1 struct fecha {
2 int dia;
3 int mes;
4 int anyo;
5 };
6 struct persona {
7 char nombre[50];
8 float altura;
9 float peso;
10 struct fecha fecha_nacimiento;
11 };

En este ejemplo el tipo struct persona contiene un miembro (fecha_nacimiento)


que es, a su vez, de otro tipo estructurado. Para acceder a un miembro de una estructura
anidada se utiliza la sintaxis:

nombre_variable.miembro.submiembro;

Por ejemplo, dada la definición de struct persona anterior, se podrı́a hacer:

1 struct persona p;
2

3 strcpy(p.nombre, "Albert Einstein");


4 p.fecha_nacimiento.dia=14;
5 p.fecha_nacimiento.mes=3;
6 p.fecha_nacimiento.anyo=1879;

6.5 Vectores de estructuras


Es muy habitual utilizar las variables de tipo estructurado como elementos de vectores.
Un vector de variables estructuradas se declara de forma similar a como se declara un
vector de cualquier otro tipo de datos:

tipo nombre_vector[dimension];

Por ejemplo, la instrucción


122
6.5 Vectores de estructuras

1 struct persona v[100];

declara 100 variables de tipo struct persona. En este caso, cada elemento del vector
v contiene una variable de tipo struct persona y, por tanto, deberá tratarse como tal.
Por ejemplo, para acceder al nombre de la persona almacenada en la primera posición del
vector emplearı́amos v[0].nombre y para acceder al peso de la última v[99].peso.

En el siguiente ejemplo se define una estructura de tipo alumno con dos miembros (nom-
bre y nota), a continuación se declara un vector de N alumnos, se introducen los datos de
cada uno, se calcula la nota media y finalmente se muestra un listado de los aprobados:

1 #include <stdio.h>
2 #define N 100
3

4 struct alumno {
5 char nombre[50];
6 float nota;
7 };
8

9 int main() {
10 struct alumno v[N]; // Declaramos un vector de 100 alumnos
11 int i;
12 float suma;
13 // Introducir datos
14 for(i=0; i<N; i++) {
15 printf("Nombre: ");
16 scanf(" %s", v[i].nombre);
17 printf("Nota: ");
18 scanf(" %f", &v[i].nota);
19 }
20

21 // Calcular nota media


22 suma=0;
23 for(i=0; i<N; i++)
24 suma += v[i].nota;
25 printf("Nota media: %f\n", suma/N);
26

27 // Mostrar aprobados
28 printf("Listado de aprobados\n");
29 for(i=0; i<N; i++) {
30 if( v[i].nota >= 5 )
31 printf(" %s\n", v[i].nombre);
32 }
33 return 0;
34 }

123
Capı́tulo 6. Estructuras

6.6 Punteros a estructuras


Los punteros a variables de tipo estructurado se utilizan de forma similar a como se em-
plean los punteros a otro tipo de variables.

Dada la variable

1 struct alumno un_alumno;

es posible almacenar su dirección de memoria en otra variable de tipo struct alumno *


(puntero a struct alumno), tal y como se muestra a continuación:

1 struct alumno un_alumno;


2 struct alumno * puntero_alumno;
3

4 puntero_alumno = &un_alumno; // Guardamos en puntero_alumno la


5 // dirección de la variable
un_alumno

A partir de este momento es posible acceder a la variable un_alumno a través de la propia


variable, o a través de su puntero.

1 un_alumno.nota = 10;

es equivalente a

1 (*puntero_alumno).nota = 10;

ya que en este contexto *puntero_alumno equivale a un_alumno.

El lenguaje C permite una sintaxis alternativa al * para el caso particular de punteros a es-
tructuras. Dado un puntero a una variable estructurada, es posible acceder a los miembros
de esa variable según la sintaxis habitual

(*puntero).miembro

o con la sintaxis alternativa

puntero->miembro

124
6.7 Paso de estructuras como parámetros

6.7 Paso de estructuras como parámetros


Las estructuras, como cualquier otro tipo de variables, pueden pasarse a una función co-
mo parámetro. A continuación se muestran distintos ejemplos de paso de estructuras co-
mo parámetros. Tal y como podrá observarse, no existen diferencias respecto al paso de
parámetros de otro tipo de variables.

6.7.1 Pasar los miembros de forma independiente


Una posibilidad consiste en pasar los distintos miembros de la estructura de forma inde-
pendiente. Esto es equivalente a pasar variables de tipos básicos.

1 #include <stdio.h>
2

3 struct alumno {
4 char nombre[50];
5 float nota;
6 };
7

8 void mostrar_alumno( char nombre[], float nota );


9

10 int main() {
11 struct alumno un_alumno;
12

13 strcpy(un_alumno.nombre, "Dennies Ritchie");


14 un_alumno.nota = 10;
15

16 // Pasamos nombre y nota de forma independiente


17 mostrar_alumno(un_alumno.nombre, un_alumno.nota);
18 return 0;
19 }
20

21 void mostrar_alumno( char nombre[], float nota ) {


22 printf(" %s %f\n", nombre, nota);
23 }

6.7.2 Pasar una estructura completa por valor


También es posible pasar toda la estructura completa, como una sola variable. En es-
te caso, y siguiendo con el ejemplo anterior, la función mostrar_alumno recibirá una
variable de tipo struct alumno.

Nota.- Para simplificar el código, en sucesivos ejemplos supondremos ya definido el tipo


struct alumno , y todas las funciones declaradas.
125
Capı́tulo 6. Estructuras

1 int main() {
2 struct alumno un_alumno;
3

4 strcpy(un_alumno.nombre, "Dennies Ritchie");


5 un_alumno.nota = 10;
6

7 // Pasamos toda la estructura de golpe


8 mostrar_alumno(un_alumno);
9 return 0;
10 }
11

12 void mostrar_alumno( struct alumno a ) {


13 printf(" %s %f\n", a.nombre, a.nota);
14 }

Es importante entender que las estructuras se pasan por valor, tal y como ocurre con las
variables de tipos básicos (o primitivos). Esto quiere decir que cualquier modificación
que hagamos sobre los miembros de la estructura no tendrá efecto fuera de la función
donde se realiza (la función que recibe la estructura opera con una copia de la misma). A
continuación se muestra un ejemplo:

1 int main() {
2 struct alumno un_alumno;
3 . . .
4 subir_nota(un_alumno);
5 . . .
6 }
7

8 void subir_nota( struct alumno a ) {


9 a.nota = a.nota + 0.5;
10 }

En este ejemplo la función subir_nota no tiene ningún efecto sobre la variable un_alumno.
Esta variable seguirá teniendo el mismo valor que tenı́a antes de la llamada a la función
subir_nota. Obviamente, aunque a la variable a de la función subir_nota le hubiéra-
mos dado el nombre un_alumno la situación serı́a exactamente la misma.

126
6.7 Paso de estructuras como parámetros

6.7.3 Pasar una estructura completa por referencia


Si queremos que una función modifique el valor de algún miembro de una variable es-
tructurada, es necesario pasar dicha variable por referencia, esto es, pasar la dirección de
memoria de la variable en lugar del valor de la misma.

1 int main() {
2 struct alumno un_alumno;
3 . . .
4 subir_nota( &un_alumno );
5 . . .
6 }
7

8 void subir_nota( struct alumno * puntero_alumno ) {


9 (*puntero_alumno).nota = (*puntero_alumno).nota + 0.5;
10

11 // O con la sintaxis equivalente


12 // puntero_alumno->nota = puntero_alumno->nota + 0.5;
13 }

En este ejemplo, a través de puntero_alumno somos capaces de modificar la variable a


la que apunta, esto es, un_alumno.

6.7.4 Pasar un vector de estructuras


El paso de un vector como parámetro a una función se realiza siempre del mismo mo-
do, independientemente de que el vector contenga variables simples o estructuradas. En
general hay que recordar que los vectores se pasan implı́citamente por referencia. Es-
to quiere decir que cualquier modificación que hagamos sobre los elementos del vector
(sean estructuras o no) se mantendrá cuando termine la función donde se realiza.

1 int main() {
2 struct alumno v[N]; // Declaramos un vector de N alumnos
3 . . .
4 subir_notas( v ); // Subir la nota a todos los alumnos
5 . . .
6 }
7 void subir_notas( struct alumno v[] ) {
8 int i;
9

10 for( i = 0; i < N; i++ )


11 v[i].nota += 0.5; // Este cambio perdurará cuando
12 // termine la función
13 }

127
Capı́tulo 6. Estructuras

6.7.5 Devolver una estructura


Las funciones también pueden devolver estructuras. Esta circunstancia, como veremos,
está relacionada con el hecho de que la asignación entre estructuras está permitida.

1 int main() {
2 struct alumno un_alumno;
3 . . .
4 un_alumno = crear_alumno();
5 . . .
6 }
7

8 struct alumno crear_alumno() {


9 struct alumno a;
10

11 printf("Nombre: ");
12 scanf(" %s", a.nombre);
13 printf("Nota: ");
14 scanf(" %f", &a.nota);
15

16 return a;
17 }

La función crear_alumno declara una variable a de tipo struct alumno. Esta variable
es devuelta al final de la función. Por tanto a la variable un_alumno declarada en la
función main se le asigna el valor de a. Esto es posible ya que, tal y como se ha comentado
en el apartado 6.3.3, el lenguaje C permite la operación de asignación entre estructuras.

6.8 Ejercicios resueltos


1. Dada la siguiente definición de un tipo estructurado:
struct complejo {
float real;
float imag;
};

implementar un programa que solicite la parte real e imaginaria de un número com-


plejo, y calcule su módulo mediante el uso de una función.
SOLUCIÓN:

1 #include <stdio.h>
2 #include <math.h>
3

128
6.8 Ejercicios resueltos

4 struct complejo {
5 float real;
6 float imag;
7 };
8

9 float modulo( struct complejo c );


10

11 int main() {
12 float m;
13 struct complejo c; // Declaración de variable
14

15 printf("Introduzca la parte real e imaginaria de un número


complejo:");
16 scanf(" %f %f", &c.real, &c.imag);
17 m = modulo(c);
18 printf("Módulo = %f\n", m);
19 return 0;
20 }
21

22 float modulo( struct complejo c ) {


23 return sqrt(c.real*c.real + c.imag*c.imag);
24 }

2. Implementar un programa que solicite el peso, altura y sexo de 10 personas y lo al-


macene en un vector de estructuras. A continuación calcular, mediante una función,
el peso medio de las personas con una altura comprendida entre 1.70 y 1.80.
SOLUCIÓN:

1 #include <stdio.h>
2 #define N 5
3

4 struct persona {
5 float peso;
6 float altura;
7 int sexo; // 1=Mujer 2=Hombre
8 };
9

10 void pedir_valores( struct persona v[] );


11 float media_peso( struct persona v[], float alt_min, float
alt_max );
12

13 int main() {
14 struct persona v[N];
15 float m;
16

17 pedir_valores(v);
129
Capı́tulo 6. Estructuras

18 m = media_peso(v, 1.7, 1.8);


19 printf("Peso medio de personas con altura entre 1.7 y 1.8:
%f\n", m);
20

21 return 0;
22 }
23

24 void pedir_valores( struct persona v[] ) {


25 int i;
26 for( i=0; i<N; i++ ) {
27 printf("Peso: ");
28 scanf(" %f", &v[i].peso);
29 printf("Altura: ");
30 scanf(" %f", &v[i].altura);
31 printf("Sexo (1=Mujer 2=Hombre): ");
32 scanf(" %d", &v[i].sexo);
33 }
34 }
35

36 float media_peso( struct persona v[], float alt_min, float


alt_max ) {
37 int i, cont = 0;
38 float suma = 0;
39 for( i=0; i<N; i++ ) {
40 if( v[i].altura >= alt_min && v[i].altura <= alt_max ) {
41 suma += v[i].peso;
42 cont++;
43 }
44 }
45 if( cont == 0 )
46 return 0;
47 else
48 return suma/cont;
49 }

3. Se supone que disponemos de cierta imagen en color almacenada en una matriz de


tamaño FIL x COL, de modo que cada elemento de la matriz define un pı́xel de la
imagen mediante 3 valores r, g, b que representan, respectivamente, la cantidad de
rojo, verde y azul del pı́xel en cuestión. Se pide:
Declarar una matriz para que pueda almacenar una imagen tal y como se ha
definido.
Implementar una función que reciba una imagen y aumente el nivel de azul
de todos sus pı́xeles en 10 unidades.
Indicar cómo se realizarı́a la llamada a la función.

130
6.9 Ejercicios propuestos

SOLUCIÓN:

1 // Definimos el tipo RGB


2 struct RGB {
3 int r;
4 int g;
5 int b;
6 };
7

8 int main() {
9 // Declaramos una matriz de tipo RGB
10 struct RGB imagen[FIL][COL];
11 . . .
12 // Llamada a la función
13 modificar_azul(imagen, 10);
14 . . .
15 }
16

17 // Implementación de la función
18 void modificar_azul( struct RGB img[FIL][COL], int x ) {
19 int i, j;
20 for(i=0; i<FIL; i++)
21 for(j=0; j<COL; j++)
22 img[i][j].b += x;
23 }

6.9 Ejercicios propuestos


1. Definir una estructura que pueda almacenar los datos de un alumno: dni, nombre,
grupo y nota (la nota de una única asignatura). A continuación, declarar:
Una variable del tipo recién creado.
100 variables del tipo recién creado.
2. Modificar la estructura del ejercicio anterior para poder almacenar, además, la di-
rección del alumno (calle, número, CP y población). Utilizar una estructura de datos
anidada.
3. Escribir un programa que lea los datos de 100 personas (nombre, dni, fecha na-
cimiento y sexo) y muestre el número de hombres y mujeres nacidos a partir de
1980.
4. Se dispone de una biblioteca de hasta 500 libros con códigos numéricos correla-
tivos desde el 1. Se quiere informatizar el proceso de mantenimiento. Escribir la
estructura de datos necesaria y las funciones para:
131
Capı́tulo 6. Estructuras

Dar de alta un libro.


Dar de baja un libro. Se debe marcar el hueco que quedará en el vector para
usarlo en la próxima inserción.
Realizar préstamos.
Recibir devoluciones de libros prestados.
Listar por pantalla, para cada libro, el código, tı́tulo, autor y ubicación.

5. En una aplicación de expedición de billetes de tren se dispone de la siguiente infor-


mación por estación:

Nombre de la estación.
Distancia en kilómetros desde esta estación a la siguiente.
Precio desde esta estación a la siguiente.

Se pide:
Definir la estructura de datos necesaria para representar la información anterior.
Según esa representación, y suponiendo almacenada en esa estructura la informa-
ción ordenada según el recorrido de la primera estación a la estación terminal, escri-
bir una función que, recibiendo como parámetros dos estaciones, escriba en pantalla
las estaciones por las que va a pasar el tren y devuelva la cantidad de kilómetros y
precio necesario para hacer el recorrido.

132
Capı́tulo 7

Gestión de ficheros

7.1 Introducción
La memoria principal de los ordenadores es volátil. Esto quiere decir que en ausencia de
corriente eléctrica la memoria pierde toda la información que tenı́a almacenada. Por otro
lado, no tendrı́a sentido introducir en un programa informático los datos de matrı́cula de
20.000 alumnos y luego no poder almacenarlos de forma permanente para su posterior
recuperación (no podemos confiar en que el ordenador vaya a estar encendido indefini-
damente). Es necesario un mecanismo que nos permita almacenar los datos de nuestros
programas para futuros usos.

Los ordenadores pueden almacenar información de forma permanente1 en lo que se de-


nomina sistemas de almacenamiento secundarios: discos, CDs, DVDs, memorias USB,
etc.

Un fichero (o archivo) es una secuencia de bytes que representan cierta información, al-
macenada permanentemente en algún soporte digital. La información se almacena en los
ficheros en forma de una secuencia de bytes terminada con el carácter especial EOF (End
Of File). El modo en que debe interpretarse la secuencia de bytes contenida en el fiche-
ro dependerá del sistema de codificación empleado. Un mismo fichero podrá contener
caracteres, números, imágenes, etc. codificados cada uno en un sistema diferente. Es res-
ponsabilidad del programador conocer qué criterios de codificación tienen las secuencias
de bytes contenidas en el fichero, para poder interpretar correctamente la información que
contiene.
1 El término “permanente” deberı́a ser interpretado con cautela, ya que ningún sistema de almacenamiento

está exento del riesgo de pérdida de información. En este sentido, las copias de seguridad en más de un medio
deberı́a ser una práctica habitual.

133
Capı́tulo 7. Gestión de ficheros

7.2 Tipos de ficheros


Cuando se trabaja con ficheros se distinguen básicamente dos formas en las que puede
almacenarse la información (lo que da lugar a dos tipos de ficheros):

Como caracteres. En este caso se entiende que la secuencia de bytes almacenada


en el fichero contiene una secuencia de caracteres representados en algún sistema
de codificación de caracteres (ASCII, Unicode, etc.). Lógicamente, este tipo de
ficheros servirán para almacenar texto (más concretamente texto plano, sin ningún
tipo de formato, ya que estos sistemas de codificación no tienen capacidad para
representar conceptos como negrita, cursiva, tamaño de la letra, etc.). Los ficheros
que contienen únicamente caracteres se denominan ficheros de texto.
Como información binaria. En este caso se entiende que la secuencia de bytes repre-
senta información codificada en cualquier sistema (no necesariamente un sistema
de codificación de caracteres). Por tanto, cualquier fichero que no contenga texto
plano diremos que es un fichero binario.

En ambos casos el fichero contiene una secuencia de bytes terminada con el carácter
especial EOF. La única diferencia radica en la forma en la que el programa que trata el
fichero debe interpretar la información en él contenida.

7.3 Operaciones con ficheros


Para trabajar con ficheros hay que realizar siempre las siguientes acciones:

1. Abrir el fichero mediante la instrucción fopen.

2. Realizar operaciones de lectura o escritura sobre el fichero.

3. Cerrar el fichero mediante la instrucción fclose.

Las operaciones de apertura y cierre de fichero se realizan de modo similar, indepen-


dientemente de que el fichero sea de texto o binario, mientras que las operaciones de
lectura/escritura serán distintas en función del tipo de fichero. En los siguientes apartados
se explican con detalle estas operaciones.

134
7.3 Operaciones con ficheros

7.3.1 Abrir y cerrar ficheros


Todos los ficheros tienen un nombre que los identifica (o, con mayor precisión, un nombre
y un directorio). El lenguaje C, sin embargo, para operar con ficheros no utiliza su nombre
sino lo que se conoce como descriptor de fichero. Estos descriptores se almacenan en
variables de un nuevo tipo de datos hasta ahora no visto denominado FILE *.

Antes de realizar cualquier operación con un fichero es necesario abrirlo mediante la


instrucción fopen. Esta instrucción asocia un nombre de fichero a un descriptor. Para
ello, recibe el nombre del fichero como parámetro y devuelve su descriptor. A partir de
este momento, y para el resto de operaciones, el nombre ya no volverá a usarse y, en su
lugar, se identificará el fichero mediante su descriptor. La instrucción fopen se usa del
siguiente modo:

descriptor = fopen("nombre_del_fichero", modo);

donde:

descriptor deberá ser una variable de tipo FILE *.

nombre_del_fichero es, obviamente, el nombre del fichero. Si este nombre no


contiene la ruta de ningún directorio, entonces se entenderá que el fichero se en-
cuentra en el directorio actual de trabajo.
modo es una cadena de caracteres que indica el modo en que se desea abrir el fichero
(lectura, escritura o añadir) y el tipo de fichero (binario o texto), tal y como se indica
en la siguiente tabla:

Texto Binario
“rt” “rb” Abre el fichero para lectura (read). El fichero debe existir.
“wt” “wb” Abre el fichero para escritura (write). Si el fichero no existe,
se crea uno nuevo, en caso contrario se borra el contenido del
fichero existente.
“at” “ab” Abre el fichero en modo añadir (append). Similar al modo
de escritura, pero no borra el contenido de los ficheros exis-
tentes, sino que añade los nuevos datos al final.
“rt+” “rb+” Abre el fichero tanto para lectura como para escritura. El
fichero debe existir.
“wt+” “wb+” Abre el fichero tanto para lectura como para escritura. Si
el fichero no existe, se crea uno nuevo, en caso contrario se
borra el contenido del fichero existente.
“at+” “ab+” Abre el fichero tanto para lectura como para añadir. Si el
fichero no existe, se crea uno nuevo.
135
Capı́tulo 7. Gestión de ficheros

Respecto a la tabla anterior debe puntualizarse que, en modo texto, puede omitirse la letra
“t”, esto es, también son válidos los modos “r”, “w”, “a”, etc. En este caso se entiende
que se desea abrir en modo texto. Por otro lado, en los ejemplos presentados a lo largo de
este capı́tulo abordaremos únicamente los modos de sólo lectura y sólo escritura, esto es,
“r”, “w”, “rb” y “wb”.

Si, por ejemplo, pretendemos abrir un fichero de texto de nombre “datos.txt” en modo
lectura, deberı́amos hacer:

1 FILE * f; // Mi descriptor de fichero


2 f = fopen("datos.txt", "r"); // Abre el fichero en modo lectura

Si un fichero no puede abrirse, la instrucción fopen devuelve el valor NULL. Es importante


comprobar si el fichero se ha abierto correctamente antes de tratar de hacer cualquier
operación con él, tal y como se muestra en el siguiente ejemplo:

1 #include <stdio.h>
2

3 int main() {
4 FILE * f;
5 f = fopen("datos.txt", "r");
6 if( f == NULL ) {
7 printf("Error abriendo el fichero datos.txt \n");
8 return 0;
9 }
10

11 // Operaciones de lectura del fichero


12 . . .
13 }

En ocasiones resulta interesante que sea el propio usuario quien indique el nombre del
fichero, en lugar de establecerlo directamente en el código del programa.

1 int main() {
2 FILE * f;
3 char nombre_fichero[512];
4

5 printf("Introduce nombre del fichero: ");


6 scanf(" %s", nombre_fichero);
7 f = fopen(nombre_fichero, "r");
8 . . .
9 }

136
7.3 Operaciones con ficheros

Cuando un fichero termina de usarse debe cerrarse siempre mediante la instrucción fclose.

fclose(descriptor_de_fichero);

7.3.2 Lectura y escritura de ficheros de texto


La lectura y escritura de texto en un fichero es muy similar a la lectura/escritura sobre la
consola. De modo muy parecido a como escribimos texto en la pantalla del ordenador,
podemos hacerlo sobre un fichero. Igualmente, podemos leer la información contenida en
un fichero de modo muy similar a como leemos los datos introducidos por teclado.

Cuando se realizan operaciones de lectura/escritura sobre ficheros han de tenerse presente


dos conceptos:

Generalmente, el fichero lo abriremos, bien en modo lectura, bien en modo escritu-


ra. Aunque existen modos de abrir un fichero para lectura y escritura a la vez, esta
forma de operar no es la habitual. En este libro nos centraremos únicamente en los
modos de sólo lectura o sólo escritura.

Tanto la lectura como la escritura se realizan de modo secuencial. Esto quiere decir
que operaciones sucesivas de escritura sobre un mismo fichero irán añadiendo el
nuevo texto a continuación del que ya habı́a, tal y como ocurre cuando escribimos
sobre la consola. Del mismo modo, en cada operación de lectura se irá leyendo de
forma consecutiva la información contenida en el fichero.

Para la lectura y escritura de ficheros de texto utilizaremos las instrucciones fscanf y


fprintf respectivamente. Su uso es idéntico al de printf y scanf, excepto que, en
este caso, debe especificarse como primer argumento el descriptor del fichero sobre el
que se desea leer/escribir. A continuación se explica el uso de estas instrucciones.

Operaciones de escritura

La sintaxis de la instrucción fprintf es la siguiente:

int fprintf(descriptor, formato [, lista_de_variables]);

donde descriptor hace referencia al descriptor del fichero obtenido mediante la función
fopen. En el parámetro formato podrán utilizarse los códigos %d, %f, %c y %s, de modo
similar a como se emplean en la función printf.

El siguiente programa de ejemplo escribe el texto “Hola Mundo” en un fichero de nombre


hola.txt.

137
Capı́tulo 7. Gestión de ficheros

1 #include <stdio.h>
2 int main() {
3 FILE * f;
4

5 f = fopen("hola.txt", "w"); // Abrir el fichero en modo


escritura
6 fprintf(f, "Hola Mundo"); // Escribir en el fichero
7 fclose(f); // Cerrar el fichero
8 return 0;
9 }

En caso de que el fichero hola.txt existiese antes de la ejecución del programa, su


contenido original habrı́a sido borrado. Si hubiésemos querido mantener los datos del
fichero original y haber añadido el nuevo texto a continuación, simplemente tendrı́amos
que haber abierto el fichero en modo append (añadir).

1 f = fopen("hola.txt", "a");

Por supuesto, se puede hacer más de un fprintf sobre el mismo fichero. Cada instruc-
ción fprintf irá añadiendo el nuevo texto a continuación del que ya tenı́amos, tal y
como se muestra en el siguiente fragmento de código:

1 f = fopen("quijote.txt", "w");
2 fprintf(f, "En un lugar de la Mancha ");
3 fprintf(f, "de cuyo nombre no quiero acordarme,\n");
4 fprintf(f, "no ha mucho tiempo que vivı́a un hidalgo ");
5 fprintf(f, "de los de lanza en astillero\n");
6 fclose(f);

Obsérvese que el carácter \n se puede utilizar exactamente igual a como se hace en la


sentencia printf.

Por otro lado, que el fichero sea de texto no quiere decir que no podamos escribir números.
Simplemente, éstos se escribirán como caracteres, utilizando la codificación ASCII, tal y
como se muestra en el siguiente ejemplo:

1 #include <stdio.h>
2 int main() {
3 FILE * f;
4 int i, j;
5

6 f = fopen("tablas_de_multiplicar.txt", "w");
7 for( i = 1; i <= 10; i++ ) {
138
7.3 Operaciones con ficheros

8 fprintf(f, "TABLA DEL %d\n", i);


9 for( j = 1; j <= 10; j++ )
10 fprintf(f, " %d x %d = %d\n", i, j, i*j);
11 }
12 fclose(f);
13 return 0;
14 }

Operaciones de lectura

La sintaxis de la instrucción scanf es la siguiente:

int fscanf(descriptor, formato [, lista_de_punteros_a_variables]);

La lectura, como se ha comentado anteriormente, se realiza de forma secuencial, comen-


zando desde el inicio del fichero. Esto quiere decir que cada operación de lectura con
fscanf irá leyendo partes o “trozos” consecutivos del fichero. Cuánta información se lee
en cada sentencia fscanf depende de el/los código/s % que se indiquen en el parámetro
formato.

Por ejemplo

1 fscanf(f, " %s", cadena);

leerá una palabra (se entiende por palabra una secuencia de caracteres delimitada por un
espacio en blanco, un tabulador, un salto de lı́nea o el carácter especial EOF). La próxima
instrucción fscanf leerá la información que aparezca a continuación de dicha palabra.

Por ejemplo, dado un fichero de texto que almacena cien números enteros, el siguiente
programa lee todos los valores del fichero y calcula su media:

1 #include <stdio.h>
2 int main() {
3 FILE * f;
4 int i, num, suma = 0;
5

6 f = fopen("numeros.txt", "r");
7 if( f == NULL ) {
8 printf("Error abriendo fichero\n");
9 return 0;
10 }
11 for( i = 1; i <= 100; i++ ) {
12 fscanf( f, " %d", &num );

139
Capı́tulo 7. Gestión de ficheros

13 suma += num;
14 }
15 printf("Media = %f\n", suma/100.0);
16 fclose(f);
17 return 0;
18 }

En el ejemplo anterior hemos leı́do un fichero de tamaño conocido, sin embargo, Es muy
habitual que no sepamos de antemano la cantidad de datos almacenados en el fichero. En
este caso habrá que ir leyendo hasta que se encuentre el carácter EOF. Para ello hay que
saber que la instrucción fscanf devuelve el número de elementos leı́dos o EOF en caso
de que se haya alcanzado el final del fichero, con lo que será fácil averiguar cuándo hemos
terminado de leer toda la información.

El siguiente ejemplo lee, carácter a carácter, el contenido completo de un fichero y lo


muestra por pantalla.

1 #include <stdio.h>
2 int main() {
3 FILE * f;
4 char caracter;
5

6 // Abrir el fichero
7 f = fopen("quijote.txt", "r");
8 if( f == NULL ) {
9 printf("Error abriendo el fichero quijote.txt\n");
10 return 0;
11 }
12

13 // Leer el fichero carácter a carácter y mostrarlo por


pantalla
14 while( fscanf(f," %c", &caracter) != EOF )
15 printf(" %c", caracter);
16

17 // Cerrar el fichero
18 fclose(f);
19 return 0;
20 }

En el ejemplo anterior se emplea un esquema muy habitual para la lectura de ficheros de


texto:

1 while( fscanf( . . . ) != EOF ) {


2 // Procesar los datos leı́dos
3 }
140
7.3 Operaciones con ficheros

Con este esquema, en la propia condición del bucle while se realiza una lectura y se com-
prueba si dicha lectura ha tenido éxito, en cuyo caso fscanf devuelve un valor distinto
de EOF. En este caso, se procesan los datos recién leı́dos y se vuelve al inicio del bucle
para realizar la siguiente lectura. En el momento en que fscanf devuelva EOF, será señal
de que hemos alcanzado el final del fichero.

En los ejemplos mostrados hasta ahora el nombre del fichero viene fijado en el programa,
pero en ocasiones puede resultar necesario que sea el usuario quien introduzca dicho
nombre. El siguiente ejemplo copia el contenido del fichero que elija el usuario sobre otro
fichero.

1 #include <stdio.h>
2 int main() {
3 FILE * f_in, * f_out;
4 char nom_fich_in[512], nom_fich_out[512], caracter;
5

6 // Pedir nombre del fichero de entrada y abrirlo en modo


lectura
7 printf("Introduce nombre de fichero a copiar: ");
8 scanf(" %s", nom_fich_in);
9 f_in = fopen(nom_fich_in, "r");
10 if( f_in == NULL ) {
11 printf("Error abriendo el fichero %s\n", nom_fich_in);
12 return 0;
13 }
14

15 // Pedir nombre del fichero de salida y abrirlo en modo


escritura
16 printf("Introduce nombre de fichero destino: ");
17 scanf(" %s", nom_fich_out);
18 f_out = fopen(nom_fich_out, "w");
19 if( f_out == NULL ) {
20 printf("Error abriendo el fichero %s\n", nom_fich_out);
21 return 0;
22 }
23

24 // Leer el fichero de entrada y volcar el contenido en el de


salida
25 while( fscanf(f_in," %c", &caracter) != EOF )
26 fprintf(f_out, " %c", caracter);
27

28 // Cerrar los ficheros


29 fclose(f_in);
30 fclose(f_out);
31

141
Capı́tulo 7. Gestión de ficheros

32 return 0;
33 }

Debe observarse que en este ejemplo, los nombres de los ficheros son introducidos por el
usuario y almacenados en cadenas de caracteres (lı́neas 8 y 17). A continuación, en las
instrucciones fopen (lı́neas 9 y 18) utilizamos como argumento estas variables en lugar
de una cadena constante.

Tal y como se ha visto ya en algún ejemplo, es perfectamente posible que un fichero de


texto contenga números. Simplemente, estarán almacenados mediante sus correspondien-
tes caracteres. En este caso es posible, a la hora de leerlos mediante fscanf, guardarlos
en nuestro programa en variables de tipos numéricos (int, float o double). El propio
fscanf hará la conversión al tipo especificado. En el siguiente ejemplo se parte de un
fichero que contiene, por cada lı́nea, el nombre de un alumno, el grupo en el que está ma-
triculado y su nota. El programa lee toda esta información y la almacena en un vector de
estructuras.

1 #include <stdio.h>
2 #define MAX_ALUMNOS 500
3

4 struct alumno {
5 char nombre[50];
6 char grupo;
7 float nota;
8 };
9

10 int cargar_datos( struct alumno v[] );


11

12 int main() {
13 struct alumno v[MAX_ALUMNOS];
14 int num_alumnos;
15 num_alumnos = cargar_datos(v);
16 . . .
17 }
18

19 int cargar_datos( struct alumno v[] ) {


20 FILE * f;
21 char nom_fich[512];
22 int i;
23

24 // Abrir fichero
25 printf("Introduce nombre de fichero de alumnos: ");
26 scanf(" %s", nom_fich);
27 f = fopen(nom_fich, "r");
28 if( f == NULL )
29 return 0; // No se han podido leer los datos
142
7.3 Operaciones con ficheros

30

31 // Leer datos del fichero


32 i = 0;
33 while( i<MAX_ALUMNOS && fscanf(f, " %s", v[i].nombre)!= EOF ) {
34 // Si no hemos alcanzado EOF tras leer el nombre,
supondremos
35 // que el fichero contiene también un grupo y una nota
36 fscanf(f, " %c", &v[i].grupo);
37 fscanf(f, " %f", &v[i].nota);
38 i++;
39 }
40

41 // Cerrar fichero
42 fclose(f);
43

44 return i; // Devolvemos el número de alumnos leı́dos


45 }

7.3.3 Lectura y escritura de ficheros binarios


Llamamos fichero binario a cualquier fichero que no contenga única y exclusivamente
texto plano. Por tanto, un fichero binario puede contener datos con distintos tipos de
codificación.

La lectura/escritura de ficheros binarios se realiza mediante las instrucciones fread y


fwrite respectivamente.

Operaciones de escritura

La función fwrite se usa del siguiente modo:

fwrite(direccion_memoria, tamaño_variable, numero_variables, descriptor);

donde:

direccion_memoria es la dirección de la variable que queremos escribir.

tamaño_variable es el número de bytes que ocupa la variable a escribir. Se pue-


de conocer el tamaño de una variable con la instrucción sizeof(variable) (o
también sizeof(tipo)).
numero_variables es la cantidad de variables que vamos a escribir. En caso de
que direccion_memoria sea la dirección de un vector, este valor podrá ser mayor
de uno.
143
Capı́tulo 7. Gestión de ficheros

descriptor es la variable de tipo FILE * que identifica al fichero.

Por ejemplo, si quisiéramos guardar en un fichero binario el valor de cierta variable entera
n, harı́amos:

1 FILE * f;
2 int n;
3 . . .
4 fwrite(&n, sizeof(int), 1, f);

Para guardar una variable de un tipo estructurado (por ejemplo una variable alum de tipo
struct alumno) ejecutarı́amos:

1 FILE * f;
2 struct alumno alum;
3 . . .
4 fwrite(&alum, sizeof(struct alumno), 1, f);

Si en lugar de tener una única variable tenemos un vector, podemos guardar el vector
entero con una única instrucción del siguiente modo:

1 FILE * f;
2 struct alumno v[N];
3 . . .
4 fwrite(v, sizeof(struct alumno), N, f);

Debe recordarse que el nombre de un vector es, en realidad, un puntero al primer elemento
del vector. Por este motivo la variable v no debe llevar, en este caso, el & delante.

Básicamente, la instrucción fwrite del ejemplo anterior podrı́a interpretarse como: .a


partir de la dirección de memoria v (esto es, &v[0]), selecciona X bytes y guárdalos en el
fichero f”, donde X=N*sizeof(struct alumno).

Operaciones de lectura

La función fread se usa del siguiente modo:

fread(puntero_variable, tamaño_variable, numero_variables, descriptor);

donde puntero_variable, tamaño_variable, numero_variables y descriptor


tienen exactamente el mismo significado que tenı́an en la instrucción fwrite.
144
7.3 Operaciones con ficheros

Básicamente, la diferencia entre fwrite y fread es el sentido en que se transfiere la


información (de memoria a fichero en el caso de fwrite y de fichero a memoria en el
caso de fread). La dirección de memoria a/desde la que se transfiere la información viene
dada por puntero_variable, mientras que la cantidad de bytes a transferir viene dada
por tamaño_variable x numero_variables.

La función fread devuelve el número de elementos leı́dos. Si el valor devuelto por fread
es cero, querrá decir que no ha sido posible llevar a cabo la lectura (normalmente porque
se ha alcanzado el final de fichero). Por ejemplo, si disponemos de un fichero de nombre
“alumnos.bin” que contiene 500 variables de tipo struct alumno, podrı́amos leerlo con
el siguiente fragmento de código:

1 FILE * f;
2 struct alumno v[N]; // Suponiendo N = 500
3 f=fopen("alumnos.bin", "rb");
4 fread(v, sizeof(struct alumno), N, f);
5 fclose(f);

Podrı́a ocurrir que no se sepa de antemano el número de alumnos almacenados en el


fichero. En este caso habrı́a que ir leyendo los alumnos del fichero uno a uno, hasta que
se alcanzase el final del fichero, tal y como se indica a continuación.

1 FILE * f;
2 struct alumno v[MAX_ALUMNOS];
3 int i = 0;
4

5 f=fopen("alumnos.bin", "rb");
6 while( i < MAX_ALUMNOS && fread(&v[i], sizeof(struct alumno), 1,
f) > 0 )
7 i++;
8 fclose(f);
9 printf("Hemos leı́do %d alumnos\n", i);

Como puede observarse en el ejemplo anterior, en cada iteración del bucle while se leen
los datos de un alumno. En el momento en que la instrucción fread devuelva cero se
saldrá del bucle, ya que esto indica que se han leı́do todos los datos del fichero. En caso
de que el fichero contenga más de MAX_ALUMNOS elementos, el bucle finalizará cuando se
alcance este número de alumnos, evitando de este modo sobrepasar el rango del vector v.

145
Capı́tulo 7. Gestión de ficheros

7.4 Acceso aleatorio


Hasta ahora se han utilizado los ficheros en modo secuencial, esto es, las distintas opera-
ciones de escritura/lectura iban guardando/”consumiendo” los datos en el fichero de forma
secuencial. Para ello el descriptor del fichero “recuerda” la posición donde se realizó la
última operación de lectura/escritura, de modo que la próxima operación se realiza siem-
pre en la siguiente posición.

En algunas ocasiones puede resultar interesante romper esta secuencialidad y leer o escri-
bir en una posición determinada del fichero. En este caso hablamos de acceso aleatorio.

La función fseek permite avanzar o retroceder en el fichero para realizar la operación


de lectura o escritura en la posición deseada (en el caso de escritura sobreescribiendo lo
que habı́a previamente en dicha posición, no insertando). El uso de la función fseek es
el siguiente:

fseek(descriptor, desplazamiento, modo);

donde descriptor es el descriptor del fichero, desplazamiento es un entero que indi-


ca la cantidad de bytes a desplazarse (puede ser positivo o negativo) y modo es otro entero
que indica a partir de dónde se realiza el desplazamiento. Para este último parámetro
pueden utilizarse las siguientes constantes definidas en stdio.h:

SEEK_SET: Desde el principio del fichero.

SEEK_CUR: Desde la posición actual.

SEEK_END: Desde el final del fichero.

El acceso aleatorio no suele utilizarse en ficheros de texto, ya que habitualmente no se


sabrá cuántos bytes ocupan nuestros datos y, por tanto, no sabremos en qué posición del
fichero se encuentran. En un fichero binario, un número entero siempre ocupará 4 bytes
(sizeof(int) bytes), independientemente de lo grande o pequeño que sea el número.
Sin embargo, en un fichero de texto, la cantidad de bytes necesaria para representar un
número puede variar (el número 123 ocupa 3 bytes, 123.00 ocupa 6 bytes y 3.141592 8
bytes). En consecuencia, la instrucción fseek no suele emplearse en ficheros de texto.

7.5 Ejercicios resueltos


1. Dado un fichero de texto de nombre ”polares.txtçon una cantidad de lı́neas desco-
nocida, en el que cada lı́nea contiene las coordenadas polares (módulo y ángulo)
de un punto, escribir un programa que lea la información contenida en dicho fiche-
ro y almacene en un segundo fichero de nombre çartesianas.txt”todos los puntos
expresados en coordenadas cartesianas.
146
7.5 Ejercicios resueltos

SOLUCIÓN:

1 #include <stdio.h>
2 #include <math.h>
3

4 int main() {
5 FILE * f_in, *f_out;
6 float mod, ang, x, y;
7

8 // Abrir fichero de entrada en modo lectura


9 f_in = fopen("polares.txt", "r");
10 if( f_in == NULL ) return 0;
11

12 // Abrir fichero de salida en modo escritura


13 f_out = fopen("cartesianas.txt", "w");
14 if( f_out == NULL ) return 0;
15

16 // Procesar datos del fichero


17 while( fscanf(f_in, " %f %f", &mod, &ang) != EOF) {
18 x = mod * cos(ang);
19 y = mod * sin(ang);
20 fprintf( f_out, " %f %f\n", x, y );
21 }
22

23 // Cerrar ficheros
24 fclose( f_in );
25 fclose( f_out );
26

27 return 0;
28 }

2. Un fichero binario contiene los datos censales de cierto número de personas (co-
mo máximo 5.000). Concretamente almacena, para cada persona, el nombre (50
caracteres), el municipio de residencia (50 caracteres) y el dı́a, mes y año de naci-
miento (enteros). Implementar una función que lea la información de dicho fichero,
la almacene en un vector de estructuras y devuelva el número de personas leı́das.
Supondremos que está definido el siguiente tipo estructurado:
struct persona {
char nombre[50];
char municipio[50];
int dia, mes, anyo;
};

Escribir también la función main en la que se declare el vector, se solicite el nombre


del fichero y se invoque a la función implementada.
SOLUCIÓN:
147
Capı́tulo 7. Gestión de ficheros

1 int cargar_datos_censales( char nom_fich[], struct persona v


[] ) {
2 FILE * f;
3 int i = 0;
4

5 f = fopen(nom_fich, "rb");
6 if( f == NULL )
7 return 0;
8 while( fread( &v[i], sizeof(struct persona), 1, f ) > 0 )
9 i++;
10 fclose(f);
11 return i;
12 }
13

14 int main() {
15 struct persona v[5000];
16 char nom_fich[50];
17 int num_personas;
18

19 // Solicitar nombre del fichero


20 printf("Introduce nombre del fichero: ");
21 scanf(" %s", nom_fich);
22

23 // Llamada a la función
24 num_personas = cargar_datos_censales(nom_fich, v);
25

26 // Resto del programa


27 . . .
28

29 return 0;
30 }

7.6 Ejercicios propuestos


1. Escribir un programa que pida al usuario el nombre de un fichero de texto y calcule
el número de caracteres y de lı́neas que contiene.
2. Dada la siguiente definición de tipo:
struct alumno {
char nombre[50];
int grupo;
float nota;
};

148
7.6 Ejercicios propuestos

Implementar una función que reciba un vector de tipo struct alumno y almacene
en el fichero “aprobados.txt” el nombre y la nota de los alumnos aprobados, y en
“suspensos.txt” el nombre y la nota de los suspendidos.

3. Se dispone de un fichero binario de nombre “alumnos” que contiene, para cada


alumno, su nombre (cadena de 50 caracteres), su grupo y su nota. Implementar un
programa que, a partir de dicho fichero, almacene en otro fichero binario de nombre
“aprobados” el nombre, grupo y nota de los aprobados.

149
Bibliografı́a

[1] B. S. Gottfried, Programación en C. Mc. Graw-Hill, 1997.

[2] H. Schildt, C. Manual de referencia. Mc. Graw-Hill, 2000.

[3] M. W. y Stephen Prata, Programación en C. Anaya Multimedia, 1990.

[4] “Programación en c - wikiversidad.” http://es.wikiversity.org/wiki/


Programaci%C3%B3n_en_C.

[5] “Programación en c - wikilibros.” http://es.wikibooks.org/wiki/


Programaci%C3%B3n_en_C.

[6] “C++ con clase - programación c++.” http://c.conclase.net.

151

Potrebbero piacerti anche