0% encontró este documento útil (0 votos)
15 vistas159 páginas

Apuntes Completos de C++

Descargar como pdf o txt
Descargar como pdf o txt
Descargar como pdf o txt
Está en la página 1/ 159

Apuntes de la asignatura:

Fundamentos de informática

José Daniel Muñoz Frı́as Rafael Palacios Hielscher

2 de abril de 2002
2
Índice general

1. Descripción del ordenador 7


1.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.2. Arquitectura de un ordenador de propósito general . . . . . . . . . . . . . . 7
1.2.1. La unidad central de proceso . . . . . . . . . . . . . . . . . . . . . . 8
1.2.2. Memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.2.3. Unidad de entrada/salida . . . . . . . . . . . . . . . . . . . . . . . . 8
1.2.4. Buses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.3. Codificación de la información . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.3.1. Sistemas de numeración posicionales . . . . . . . . . . . . . . . . . 9
1.3.2. Codificación de números en un ordenador . . . . . . . . . . . . . . . 11
1.3.3. Codificación de caracteres alfanuméricos . . . . . . . . . . . . . . . 12
1.4. Codificación del programa . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.4.1. Compiladores e intérpretes . . . . . . . . . . . . . . . . . . . . . . . 14

2. El primer programa en C 17
2.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.2. El sistema operativo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.3. Creación de un programa en C . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.3.1. Primer paso: Edición del programa fuente . . . . . . . . . . . . . . . 18
2.3.2. Segundo paso: Compilación . . . . . . . . . . . . . . . . . . . . . . 18
2.3.3. Tercer paso: Enlazado . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.4. Nuestro primer programa en C . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.4.1. Comentarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.4.2. Directivas del preprocesador . . . . . . . . . . . . . . . . . . . . . . 20
2.4.3. La función principal . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.4.4. Las funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.4.5. Finalización del programa . . . . . . . . . . . . . . . . . . . . . . . 22
2.4.6. La importancia de la estructura . . . . . . . . . . . . . . . . . . . . . 22

3. Tipos de datos 25
3.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.2. Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.3. Tipos básicos de variables . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
3.3.1. Números enteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
3.3.2. Números reales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
3.3.3. Variables de carácter . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.4. Más tipos de variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.5. Constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27

3
4 ÍNDICE GENERAL

3.6. Tamaño de las variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29


3.7. Escritura de variables en pantalla . . . . . . . . . . . . . . . . . . . . . . . . 29
3.7.1. Escritura de variables de tipo entero . . . . . . . . . . . . . . . . . . 30
3.7.2. Escritura de variables de tipo real . . . . . . . . . . . . . . . . . . . 31
3.7.3. Escritura de variables de tipo carácter . . . . . . . . . . . . . . . . . 32
3.7.4. Escritura de variables con formato . . . . . . . . . . . . . . . . . . . 32
3.8. Lectura de variables por teclado . . . . . . . . . . . . . . . . . . . . . . . . 33

4. Operadores en C 35
4.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
4.2. El operador de asignación . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
4.3. Operadores para números reales . . . . . . . . . . . . . . . . . . . . . . . . 35
4.4. Operadores para números enteros . . . . . . . . . . . . . . . . . . . . . . . . 36
4.4.1. Operador resto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
4.4.2. Operadores de incremento y decremento . . . . . . . . . . . . . . . . 37
4.5. Operador unario - . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
4.6. De vuelta con el operador de asignación . . . . . . . . . . . . . . . . . . . . 38
4.7. Conversiones de tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
4.7.1. El operador asignación y las conversiones . . . . . . . . . . . . . . . 39
4.8. Ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40

5. Control de flujo: for 43


5.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
5.2. Sintaxis del bucle for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
5.2.1. Operadores relaciones . . . . . . . . . . . . . . . . . . . . . . . . . 45
5.3. Doble bucle for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46

6. Control de flujo: while, do-while 49


6.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
6.2. Sintaxis del bucle while . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
6.2.1. Ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
6.3. Sintaxis del bucle do-while. . . . . . . . . . . . . . . . . . . . . . . . . . . 53
6.3.1. Ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
6.4. ¿Es el bucle for igual que el while? . . . . . . . . . . . . . . . . . . . . . . 55
6.4.1. Entonces ¿cuál elijo? . . . . . . . . . . . . . . . . . . . . . . . . . . 56
6.5. El ı́ndice del bucle for sirve para algo . . . . . . . . . . . . . . . . . . . . . 56
6.6. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57

7. Control de flujo: if 59
7.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
7.2. Sintaxis del bloque if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
7.3. Formato de las condiciones . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
7.3.1. Operadores relacionales . . . . . . . . . . . . . . . . . . . . . . . . 61
7.3.2. Operadores lógicos . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
7.4. Valores de verdadero y falso . . . . . . . . . . . . . . . . . . . . . . . . . . 63
7.5. Bloque if else-if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
7.6. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
ÍNDICE GENERAL 5

8. Control de flujo: switch-case, break y continue 67


8.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
8.2. La construcción switch-case . . . . . . . . . . . . . . . . . . . . . . . . . 67
8.2.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
8.2.2. Sintaxis de la construcción switch-case . . . . . . . . . . . . . . . 68
8.2.3. Ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
8.3. La sentencia break . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
8.3.1. La sentencia break y el switch-case . . . . . . . . . . . . . . . . . 73
8.3.2. La sentencia break y los bucles . . . . . . . . . . . . . . . . . . . . 74
8.3.3. Ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
8.4. La sentencia continue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
8.4.1. Ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
8.5. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79

9. Vectores y Matrices 81
9.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
9.2. Vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
9.3. Cadenas de caracteres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
9.3.1. Cadenas de caracteres de longitud variable . . . . . . . . . . . . . . 84
9.3.2. Funciones estándar para manejar cadenas de caracteres . . . . . . . . 86
9.4. Matrices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
9.5. Utilización de #define con vectores y matrices . . . . . . . . . . . . . . . . 92
9.6. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95

10. Funciones 97
10.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
10.2. Estructura de una función . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
10.3. Prototipo de una función . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
10.4. Ejemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
10.5. Paso de argumentos a las funciones. Variables locales . . . . . . . . . . . . . 102
10.6. Salida de una función y retorno de valores . . . . . . . . . . . . . . . . . . . 104
10.7. Funciones sin argumentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
10.8. Ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
10.8.1. Elevar al cuadrado . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
10.8.2. Factorial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
10.9. Paso de vectores, cadenas y matrices a funciones . . . . . . . . . . . . . . . 108
10.9.1. Paso de vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
10.9.2. Paso de cadenas a funciones . . . . . . . . . . . . . . . . . . . . . . 111
10.9.3. Paso de matrices a funciones . . . . . . . . . . . . . . . . . . . . . . 111

11. Punteros 113


11.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
11.2. Declaración e inicialización de punteros . . . . . . . . . . . . . . . . . . . . 113
11.2.1. El operador unario & . . . . . . . . . . . . . . . . . . . . . . . . . . 114
11.2.2. El operador unario * . . . . . . . . . . . . . . . . . . . . . . . . . . 115
11.3. Operaciones con punteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
11.4. Punteros y vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
11.4.1. Equivalencia de punteros y vectores . . . . . . . . . . . . . . . . . . 119
11.5. Punteros y funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
11.5.1. Retorno de más de un valor por parte de una función . . . . . . . . . 122
6 ÍNDICE GENERAL

11.6. Asignación dinámica de memoria . . . . . . . . . . . . . . . . . . . . . . . 122


11.6.1. Las funciones calloc() y malloc() . . . . . . . . . . . . . . . . . 123
11.6.2. La función free() . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
11.6.3. Ejemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125

12. Archivos 129


12.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
12.2. Apertura de archivos. La función fopen() . . . . . . . . . . . . . . . . . . . 129
12.3. Cierre de archivos. La función fclose(). . . . . . . . . . . . . . . . . . . . 131
12.4. Lectura y escritura en archivos. Las funciones fprintf() y fscanf(). . . . 132
12.4.1. La función fprintf() . . . . . . . . . . . . . . . . . . . . . . . . . 132
12.4.2. La función fscanf() . . . . . . . . . . . . . . . . . . . . . . . . . . 132
12.5. Ejemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
12.6. Funciones de entrada y salida a archivo sin formato . . . . . . . . . . . . . . 138

13. Estructuras de datos 141


13.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
13.2. Declaración y definición de estructuras . . . . . . . . . . . . . . . . . . . . . 141
13.3. La sentencia typedef . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
13.4. Acceso a los miembros de una estructura . . . . . . . . . . . . . . . . . . . . 143
13.5. Ejemplo: Suma de números complejos . . . . . . . . . . . . . . . . . . . . . 144
13.6. Estructuras y funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
13.7. Punteros a estructuras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
13.7.1. Paso de punteros a estructuras a funciones . . . . . . . . . . . . . . . 147
13.8. Vectores de estructuras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
13.8.1. Ejemplo: Cálculo del producto escalar de dos vectores de complejos . 149
13.9. Estructuras y archivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
13.9.1. Diferencia entre archivos de texto y archivos binarios . . . . . . . . . 152
13.9.2. Para almacenar estructuras en archivos de texto . . . . . . . . . . . . 153
13.9.3. Para almacenar estructuras en archivos binarios . . . . . . . . . . . . 154
13.9.4. Funciones fread, fwrite y fseek . . . . . . . . . . . . . . . . . . 156
Capı́tulo 1

Descripción del ordenador

1.1. Introducción
Un ordenador es un equipo de procesamiento de datos. Su tarea consiste en, a partir de
unos datos de entrada, generar otros datos de salida, tal como se muestra en la figura 1.1.a. La
principal ventaja de un ordenador radica en que el procesamiento que realiza con los datos no
es fijo, sino que responde a un programa previamente introducido en él, tal como se muestra en
la figura 1.1.b. Esta propiedad dota a los ordenadores de una gran flexibilidad, permitiendo que
una misma máquina sirva para fines tan dispares como diseñar circuitos electrónicos, resolver
problemas matemáticos o, por qué no, jugar a los marcianitos. Nótese también que debido a
esto un ordenador sin un programa que lo controle es un cacharro totalmente inútil.
El objetivo de esta asignatura es enseñar al alumnos los fundamentos del proceso de pro-
gramación de un ordenador usando el lenguaje C.

Programa

Datos de Datos de Datos de Datos de


Entrada Salida Entrada Salida
Ordenador Ordenador

a b

Figura 1.1: Proceso de información en un ordenador

1.2. Arquitectura de un ordenador de propósito general


En la figura 1.2 se muestra un diagrama de bloques de un ordenador de propósito general,
en el que se pueden observar tres bloques principales: la Unidad Central de proceso o CPU 1 ,
la memoria y el sistema de entrada/salida. Todos estos elementos se comunican mediante
un conjunto de conexiones eléctricas denominadas buses.

1 Del inglés Central Processing Unit.

7
8 CAPÍTULO 1. DESCRIPCIÓN DEL ORDENADOR

Bus de direcciones

Bus de Datos Discos

C.P.U. Memoria E/S Pantalla

Bus de control

Figura 1.2: Diagrama de bloques de un ordenador

1.2.1. La unidad central de proceso


La unidad central de proceso, también llamada CPU o simplemente procesador, es la
unidad responsable de realizar todo el procesamiento de información. Para ello lee un progra-
ma de la memoria y actúa según las instrucciones de dicho programa. Dichas instrucciones
son muy simples: leer datos de la memoria, realizar operaciones matemáticas y lógicas simples
(sumas, comparaciones. . . ), escribir resultados en la memoria, etc.

1.2.2. Memoria
Es la unidad encargada de almacenar tanto el programa que le dice a la CPU lo que tiene
que hacer, como los datos con los que tiene que trabajar. Desde el punto de vista del progra-
mador consiste en un vector de posiciones de memoria a las cuales se puede acceder (leer o
escribir) sin más que especificar su dirección.

1.2.3. Unidad de entrada/salida


Esta unidad se encarga de comunicar al ordenador con el mundo exterior y con los disposi-
tivos de almacenamiento secundario (discos). En algunas arquitecturas aparece como una serie
de posiciones de memoria más, indistinguibles por el programador de la memoria normal, y
en otras está en un espacio de direcciones separado.

1.2.4. Buses
La interconexión entre los elementos del ordenador se realiza mediante un bus de datos,
gracias al cual el procesador lee o escribe datos en el resto de dispositivos 2 , un bus de direc-
ciones por el cual el procesador indica a los dispositivos qué posición quiere leer o escribir 3 ,
un bus de control mediante el cual el procesador les indica el momento en el que va a realizar
el acceso, si éste va a ser de lectura o escritura, etc. Este bus de control también permite a los
2 Se dice entonces que este bus es bidireccional, pues permite que la información viaje desde el procesador a los

dispositivos o viceversa.
3 Este bus en cambio es unidireccional, pues la dirección viaja siempre del procesador a los dispositivos.
1.3. CODIFICACIÓN DE LA INFORMACIÓN 9

dispositivos pedir la atención del procesador ante un suceso mediante un mecanismo llamado
interrupción.

1.3. Codificación de la información


Todas las unidades estudiadas en la Sección 1.2 están formadas por circuitos electró-nicos
digitales que se comunican entre sı́. Como aprenderán en la asignatura de electrónica digital,
estos circuitos trabajan con señales que toman sólo dos valores: encendido-apagado, cargado-
descargado, tensión alta-tensión baja. A estos dos estados diferenciados se les asignan los
valores binarios 0 y 1. Por tanto, dentro de un ordenador, todo discurre en forma de dı́gitos
binarios o bits4 . Por el contrario en la vida real casi ningún problema está basado en números
binarios, y la mayorı́a ni siquiera en números. Por tanto es necesario establecer una corre-
spondencia entre las magnitudes binarias con las que trabaja el ordenador y las magnitudes
que existen en el mundo real. A esta correspondencia se le denomina codificaci ón.
Si las operaciones a realizar son funciones lógicas (AND, OR, NOT. . . ) la codificación es
muy simple: al valor falso se le asigna el dı́gito binario 0 y a la condición cierto se le asigna el
1.
Si en cambio hemos de realizar operaciones matemáticas, la codificación de números es
un poco más compleja. Antes de estudiar cómo se codifican los números en un ordenador,
vamos a estudiar un poco de matemáticas (¡pero que no cunda el pánico!).

1.3.1. Sistemas de numeración posicionales


Al sistema de numeración que usamos dı́a a dı́a se le denomina posicional porque cada
número está formado por una serie de dı́gitos de forma que cada dı́gito tiene un peso en
función de su posición. El valor del número se forma por la suma del producto de cada dı́gito
por su peso, ası́ por ejemplo cuando se escribe el número 1327, en realidad se esta diciendo:

1327 1 

1000  3 

100  2 

10  7 

1 1 

103  3 

102  2 

101  7 

100 (1.1)

Como se puede apreciar, los pesos por los que se multiplica cada dı́gito se forman elevando
un número, al que se denomina base a la potencia indicada por la posición del dı́gito. Al dı́gito
que está más a la derecha se le denomina dı́gito menos significativo y se le asigna la posición
0; al que está más a la izquierda se le denomina dı́gito más significativo.
Como en un sistema digital sólo existen dos dı́gitos binarios, 0 y 1, los números dentro de
un ordenador han de representarse en base 2 5 , ası́ por ejemplo6 :

110112 1 

24  1 

23  0 

22  1 

21  1 

20
(1.2)
1 

16  1 

8  0 

4  1 

2  1 

1 27

Esta base es muy adecuada para las máquinas, pero bastante desagradable para los hu-
manos, por ejemplo el número 1327 en binario es 10100101111 2; bastante más difı́cil de
recordar, escribir e interpretar. Por ello los humanos, que para eso somos más listos que las
máquinas, usamos sistemas de numeración más aptos a nuestras facultades. La base 10 es la
4 La palabra bit viene del inglés binary digit.
5 Para representar números en una base n hacen falta n sı́mbolos distintos.
6 Para indicar números representados en una base distinta a la base 10, se pondrá en estos apuntes la base como

subı́ndice.
10 CAPÍTULO 1. DESCRIPCIÓN DEL ORDENADOR

más cómoda para operar con ella debido a que las potencias de la base son fáciles de calcular
y a que la suma expresada en (1.1) se calcula sin ninguna dificultad. Por el contrario en base 2
ni las potencias de la base ni la suma final (1.2) son tan fáciles de calcular.
Uno de los inconvenientes de la representación de números en binario es su excesiva longi-
tud. Para simplificar las cosas se pueden convertir los números binarios a decimales y trabajar
con ellos, pero eso no es tarea fácil, pues hay que realizar una operación como la indicada en
(1.2). Para solucionar este problema, se pueden usar bases que son también potencias de 2,
de forma que la notación sea compacta y fácil de manejar por los humanos y que además las
conversiones a binario sean fáciles de realizar. Las dos bases más usadas son la octal (base 8) y
la hexadecimal (base 16). En la tabla 1.1 se muestran las equivalencias entre binario, decimal,
octal y hexadecimal.
Cuadro 1.1: Conversión entre bases
Binario Decimal Octal Hexadecimal
0000 0 0 0
0001 1 1 1
0010 2 2 2
0011 3 3 3
0100 4 4 4
0101 5 5 5
0110 6 6 6
0111 7 7 7
1000 8 10 8
1001 9 11 9
1010 10 12 A
1011 11 13 B
1100 12 14 C
1101 13 15 D
1110 14 16 E
1111 15 17 F
10000 16 20 10
10001 17 21 11
Como se puede apreciar, los números octales sólo usan 8 sı́mbolos (del 0 al 7) y la base
hexadecimal precisa 16 (del 0 al 9 y se añaden las letras de la A a F).
La conversión entre binario y octal se realiza agrupando los bits en grupos de tres y real-
izando la transformación de cada grupo por separado, según la tabla 1.1:

101001011112 10 100 101 111 24578


y entre binario y hexadecimal se hace igual, pero agrupando los bits de cuatro en cuatro:

101001011112 101 0010 1111 52F16


La notación octal se usó en los ordenadores primitivos, en los que el ordenador comuni-
caba los resultados con lamparitas (como salen en las pelı́culas) que se agrupaban en grupos
de 3 para su interpretación en base octal. Sin embargo, con el avance de la técnica, se vio
que era conveniente usar números binarios de 8 bits, a los que se les denominó bytes. Estos
bytes se representan mucho mejor como dos dı́gitos hexadecimales, uno para cada grupo de 4
bits7 . El caso es que hoy en dı́a la notación octal está francamente en desuso, siendo lo más
7 además a cada grupo de 4 bits, representable directamente por un dı́gito hexadecimal, se le denomina nible.
1.3. CODIFICACIÓN DE LA INFORMACIÓN 11

corriente expresar un número binario mediante su equivalente hexadecimal cuando ha de ser


interpretado por un ser humano (dentro del ordenador por supuesto todo esta en binario).

1.3.2. Codificación de números en un ordenador


Una de las aplicaciones principales de los ordenadores es la realización de cálculos matemáticos,
cálculos que pueden ser tan simples como restar las 1.000 pesetas de una cuenta bancaria cuan-
do se sacan de un cajero o tan complejos como una simulación de la combustión en el cilindro
de un motor de explosión. Sin embargo se ha visto que los ordenadores sólo saben usar el
0 y el 1. Por tanto es necesario buscar métodos para codificar los números en un ordenador.
La tarea no es tan fácil como pueda parecer a primera vista. En primer lugar ¿cuantos tipos
de números existen? Pues tenemos en primer lugar los números naturales, que son los más
simples. Si usamos restas nos hacen falta los números negativos. Si queremos dividir, salvo
que tengamos mucha suerte y la división sea exacta, hemos de usar los números racionales. Si
además tenemos que hacer raı́ces cuadradas o usar números tan apasionantes como el π, nos
las tendremos que ver con los irracionales. Incluso si se nos ocurre hacer una raı́z cuadrada de
un número negativo tenemos que introducirnos en las maldades de los números imaginarios.
Los lenguajes de programación de propósito general como el C o el Pascal, permiten traba-
jar tanto con números enteros como con números reales. Otros lenguajes como el FORTRAN,
más orientados al cálculo cientı́fico, también pueden trabajar con números imaginarios.

Codificación de números enteros

Si se desea codificar un número natural, la manera más directa es usar su representación en


binario, tal como se ha visto en la Sección 1.3.1. No obstante, los procesadores trabajan con
magnitudes de un número de bits predeterminados, tı́picamente múltiplos pares de un byte.
Ası́ por ejemplo los procesadores tipo Pentium pueden trabajar con números de 1, 2 y 4 bytes,
o lo que es lo mismo, de 8, 16 y 32 bits. Por tanto, antes de introducir un número natural en
el ordenador, hemos de averiguar el número de bits que hacen falta para codificarlo, y luego
decirle al ordenador que use un tipo de almacenamiento adecuado.
Para averiguar cuantos bits hacen falta para almacenar un número natural no hace falta
convertirlo a binario. Si nos fijamos en que con n bits se pueden representar números naturales
que van desde 0 a 2n 1, sólo hace falta comparar el número a codificar con el rango del tipo
en el que lo queremos codificar. Ası́ por ejemplo si queremos almacenar el número 327 en
8 bits, como el rango de este tipo es de 0 a 28 1 255, vemos que en 8 bits no cabe, por
tanto hemos de almacenarlo en 16 bits, que al tener un rango de 0 a 2 16 1 65535 nos vale
perfectamente.
Para la codificación de números negativos existen varios métodos, aunque el más usado
es la codificación en complemento a dos, que posee la ventaja de que la operación suma es
la misma que para los números positivos. En esta codificación el bit más significativo vale
cero para los números positivos y uno para los negativos. En el resto de bits está codificada la
magnitud, aunque el método de codificación se escapa de los fines de esta introducción. Lo que
si ha de quedar claro es que como ahora tenemos en n bits números positivos y negativos, el
rango total de 0 a 2n 1 se divide ahora en 2n 1 a 2n 1 1. Ası́ pues si por ejemplo queremos
 

codificar el número -227 necesitaremos 16 bits, pues el rango de un número codificado en


complemento a dos de 8 bits es de 27 a 27 1, es decir, de 128 a 127. En cambio el rango
disponible en 16 bits es de 32768 a 32767.
12 CAPÍTULO 1. DESCRIPCIÓN DEL ORDENADOR

Codificación de números reales


Para la mayorı́a de las aplicaciones de cálculo cientı́fico se necesita trabajar con números
reales. Estos números reales se representan según una mantisa de parte entera 0 y un ex-
ponente (por ejemplo 3.701 se almacena como 0 3701 10 1). Nuevamente tanto la mantisa


como el exponente se almacenan en binario, según una codificación que escapa de los fines
de esta breve introducción. Antiguamente cada ordenador usaba un tipo de codificación en
punto flotante distinta. Por ello surgió la necesidad de buscar un estándar de almacenamiento
común para todas las plataformas. De ahı́ surgió el estándar IEEE 854 8 que es el usado ac-
tualmente por la mayorı́a de los procesadores para la realización de las operaciones en coma
flotante. Este estándar define tanto la codificación de los números como la manera de realizar
las operaciones aritméticas con ellos (incluidos los redondeos).

1.3.3. Codificación de caracteres alfanuméricos


Aunque el proceso matemático es una parte importante dentro de un ordenador, la may-
orı́a de los problemas en el mundo real requieren la posibilidad de trabajar con texto. Por tanto
es necesario buscar un método para codificar los caracteres alfanuméricos dentro de un orde-
nador. La manera de realizar está codificación es asignar a cada carácter un valor numérico,
totalmente arbitrario, almacenando las correspondencias carácter-número en una tabla.
Al principio existieron varias codificaciones de caracteres, pues los fabricantes, como
siempre, no se pusieron de acuerdo. Por ello nació el estándar ASCII que son las siglas de
“American Standard Code for Information Interchange”. Como su propio nombre indica este
estándar es americano, y como éstos son muy dados a creer que el mundo termina donde lo
hacen sus fronteras, usaron sólo números de 7 bits (0 a 127) para almacenar sus caracteres y
no codificaron caracteres tan estupendos como la ñ o la á. Esto ha dado lugar a que, incluso
hoy en dı́a, los que tenemos la “desgracia” de llamarnos Muñoz, nos vemos con frecuencia


rebautizados a Mu oz. Para solucionar tan dramática situación, se extendieron los 7 bits orig-
inales a 8, usando los 128 nuevos caracteres para codificar los caracteres de las lenguas no
inglesas. El problema fue que nuevamente no se pusieron de acuerdo los fabricantes y cada
uno lo extendı́a a su manera, con el consiguiente problema. Para aliviar esto, surgió el estándar
ISO 88599, que en los 128 primeros caracteres coincide con el ASCII y en los restantes 128
contiene extensiones para la mayorı́a de los lenguajes. Como no caben todos los caracteres de
todas las lenguas en sólo 128 posiciones, se han creado 10 alfabetos ISO 8859, de los cuales el
primero (ISO 8859-1 Latin-1) contiene los caracteres necesarios para codificar los caracteres
de las lenguas usadas en Europa occidental.
A continuación se muestra una tabla ASCII tomada de un sistema Unix:

Oct Dec Hex Char Oct Dec Hex Char


------------------------------------------------------------
000 0 00 NUL ’\0’ 100 64 40 @
001 1 01 SOH 101 65 41 A
002 2 02 STX 102 66 42 B
003 3 03 ETX 103 67 43 C
004 4 04 EOT 104 68 44 D
8 IEEE son las siglas del Institute of Electrical and Electronics Engineers, que es una asociación americana de

ingenieros que se dedica entre otras tareas a la definición de estándares relacionados con la ingenierı́a eléctrica y
electrónica.
9 ISO son las siglas de International Standars Organization.
1.3. CODIFICACIÓN DE LA INFORMACIÓN 13

005 5 05 ENQ 105 69 45 E


006 6 06 ACK 106 70 46 F
007 7 07 BEL ’\a’ 107 71 47 G
010 8 08 BS ’\b’ 110 72 48 H
011 9 09 HT ’\t’ 111 73 49 I
012 10 0A LF ’\n’ 112 74 4A J
013 11 0B VT ’\v’ 113 75 4B K
014 12 0C FF ’\f’ 114 76 4C L
015 13 0D CR ’\r’ 115 77 4D M
016 14 0E SO 116 78 4E N
017 15 0F SI 117 79 4F O
020 16 10 DLE 120 80 50 P
021 17 11 DC1 121 81 51 Q
022 18 12 DC2 122 82 52 R
023 19 13 DC3 123 83 53 S
024 20 14 DC4 124 84 54 T
025 21 15 NAK 125 85 55 U
026 22 16 SYN 126 86 56 V
027 23 17 ETB 127 87 57 W
030 24 18 CAN 130 88 58 X
031 25 19 EM 131 89 59 Y
032 26 1A SUB 132 90 5A Z
033 27 1B ESC 133 91 5B [
034 28 1C FS 134 92 5C \ ’\\’
035 29 1D GS 135 93 5D ]
036 30 1E RS 136 94 5E ˆ
037 31 1F US 137 95 5F _
040 32 20 SPACE 140 96 60 ‘
041 33 21 ! 141 97 61 a
042 34 22 " 142 98 62 b
043 35 23 # 143 99 63 c
044 36 24 $ 144 100 64 d
045 37 25 % 145 101 65 e
046 38 26 & 146 102 66 f
047 39 27 ’ 147 103 67 g
050 40 28 ( 150 104 68 h
051 41 29 ) 151 105 69 i
052 42 2A * 152 106 6A j
053 43 2B + 153 107 6B k
054 44 2C , 154 108 6C l
055 45 2D - 155 109 6D m
056 46 2E . 156 110 6E n
057 47 2F / 157 111 6F o
060 48 30 0 160 112 70 p
061 49 31 1 161 113 71 q
062 50 32 2 162 114 72 r
063 51 33 3 163 115 73 s
064 52 34 4 164 116 74 t
065 53 35 5 165 117 75 u
066 54 36 6 166 118 76 v
14 CAPÍTULO 1. DESCRIPCIÓN DEL ORDENADOR

067 55 37 7 167 119 77 w


070 56 38 8 170 120 78 x
071 57 39 9 171 121 79 y
072 58 3A : 172 122 7A z
073 59 3B ; 173 123 7B {
074 60 3C < 174 124 7C |
075 61 3D = 175 125 7D }
076 62 3E > 176 126 7E ˜
077 63 3F ? 177 127 7F DEL

Por ejemplo el carácter ’a’ se codifica como el número 97 (01100001 en binario) y el


carácter ’A’ se codifica como 65 (00100001 en binario).
También cabe destacar que el orden numérico dentro de la tabla coincide con el orden
alfabético de las letras, lo cual es muy útil cuando se desean ordenar caracteres por orden
alfabético.

1.4. Codificación del programa


En la sección 1.1 se dijo que para que un ordenador realice cualquier tarea, es necesario
decirle cómo debe realizarla. Esto se hace mediante un programa, el cual está compuesto
por instrucciones muy simples. Estas instrucciones también están codificadas (en binario por
supuesto), según el denominado juego de instrucciones del procesador. Cada procesador tiene
su propio juego de instrucciones y su propia manera de codificarlas, que dependen de la ar-
quitectura interna del procesador. Ası́ por ejemplo en un microprocesador 68000 de Motorola
la instrucción para sumar 7 a una posición de memoria indicada por el registro interno A2 es
0101111010010010.
En los primeros ordenadores los programas se codificaban directamente en binario, lo cual
era muy penoso y propenso a errores. Por suerte los ordenadores de entonces no eran muy
potentes y los programas por tanto no eran demasiado sofisticados. Aun ası́, como al hombre
nunca le han gustado las tareas repetitivas, pronto se vio la necesidad de automatizar un poco
el proceso. Para ello a cada instrucción se le asigna un mnemónico fácil de recordar por el
hombre y se deja a un programa la tarea de convertir estos mnemónicos a sus equivalentes
en binario legibles por el procesador. El ejemplo de antes se escribirı́a ahora como ADDQ.B
#7,(A2), lo cual es mucho más legible. A este lenguaje se le denomina lenguaje ensamblador,
aunque a veces también se le denomina código maquina.
El problema de la programación en ensamblador es que este lenguaje es distinto para cada
procesador, pues esta muy unido a su arquitectura interna. Por ello si tenemos un programa
escrito en ensamblador para un 68000 y decidimos ejecutarlo en un Pentium, tendremos que
reescribirlo de nuevo. Para solucionar este problema (o al menos paliarlo bastante), se desar-
rollaron los lenguajes de alto nivel. Estos lenguajes parten de una arquitectura de procesador
genérica, como la que se ha introducido en este capı́tulo, y definen un lenguaje independiente
del procesador, por lo que una vez escrito el programa, éste se puede ejecutar prácticamente
sin cambios en cualquier procesador.

1.4.1. Compiladores e intérpretes


En un lenguaje de alto nivel es necesario traducir el programa que introduce el usuario
al código máquina del ordenador. Esta traducción se puede realizar al mismo tiempo de la
ejecución, de forma que se traduce cada lı́nea del programa de alto nivel y después se ejecuta.
1.4. CODIFICACIÓN DEL PROGRAMA 15

Como ejemplos de lenguajes que son normalmente interpretados tenemos el BASIC y el Java.
El principal inconveniente de los lenguajes interpretados es que el proceso de traducción lleva
tiempo, y hay que realizarlo una y otra vez.
Si se desea eliminar el tiempo de traducción del programa de alto nivel, podemos traducirlo
todo de una vez y generar un programa en código máquina que será ejecutable directamente
por el procesador. Con ello la ejecución del programa será mucho más rápida, pero tiene como
inconveniente que cada vez que se cambia algo en el programa hay que volver a traducirlo a
código máquina. Al proceso de traducción del programa a código máquina se le denomina
compilación, y al programa encargado de ello compilador. Ejemplos de lenguajes que son
normalmente compilados son el C, el Pascal o el FORTRAN.
16 CAPÍTULO 1. DESCRIPCIÓN DEL ORDENADOR
Capı́tulo 2

El primer programa en C

2.1. Introducción
Una vez descrito el funcionamiento básico de un ordenador, vamos a realizar nuestro
primer programa en lenguaje C. Veremos en este capı́tulo las herramientas necesarias para
crear programas, almacenarlos, compilarlos y ejecutarlos.

2.2. El sistema operativo


En el capı́tulo anterior se ha descrito muy someramente el funcionamiento del ordenador.
Se vio que existı́an unidades de entrada salida como el teclado o la pantalla y unidades de
almacenamiento secundario como discos o CDROM. El manejo de estos dispositivos es al-
tamente complejo, sobre todo para los programadores noveles, y además está estrechamente
ligado al funcionamiento fı́sico de los dispositivos, por lo que si se cambia el dispositivo, varı́a
la forma de usarlo. Para facilitar la vida al programador, todas las tareas “sucias” del orde-
nador como son entre otras la gestión de la pantalla, teclado o accesos a discos las realiza el
sistema operativo. Para ello, los sistemas operativos poseen una serie de funciones que hacen
de interfaz entre el programador y el sistema, que se denominan interfaz del programador
de aplicaciones, y comúnmente se conoce con las siglas inglesas API 1 .
El sistema operativo también se encarga de interpretar las órdenes que el usuario le da,
bien mediante una interfaz de comandos como en MS-DOS o UNIX o bien mediante una
interfaz gráfica como en Windows 95 o en X Window System. Esto permite al usuario decirle
al sistema que ejecute un programa, que borre un archivo, que lo copie, que se conecte a
Internet. . .

2.3. Creación de un programa en C


El proceso de creación de un programa en C, ilustrado en la figura 2.1, consta de los
siguientes pasos:

1 De Application Programmer Interface

17
18 CAPÍTULO 2. EL PRIMER PROGRAMA EN C

Figura 2.1: Compilación de un programa. Tomado de [Antonakos and Mansfield, 1997].

2.3.1. Primer paso: Edición del programa fuente


El primer paso a realizar para la creación de un programa es escribirlo. Para ello se necesita
una herramienta llamada editor de texto, como por ejemplo el edit de MSDOS, el notepad
de Windows o el vi de UNIX. Todos estos programas permiten al usuario introducir un texto
en el ordenador, modificarlo y luego guardarlo en un archivo en el disco duro para su posterior
recuperación o para que sea usado por otros programas (como por ejemplo el compilador).
Al archivo creado se le denomina programa fuente o también código fuente.
Tı́picamente se emplea una extensión al nombre del archivo para indicar su contenido. En
el caso de archivos que contienen código fuente en C, el nombre ha de tener extensión .c. Por
ejemplo en la figura 2.1 el código fuente que se compila se llama MYPROG.C.

2.3.2. Segundo paso: Compilación


Una vez creado el programa fuente es necesario traducirlo. De ello se encarga un progra-
ma llamado compilador, el cual tomando como entrada el programa fuente y los ficheros
cabecera (normalmente con extensión .h), los traduce a código máquina creando lo que se
2.4. NUESTRO PRIMER PROGRAMA EN C 19

denomina código objeto (con extensión .obj ó .o). Si existe algún error sintáctico en el códi-
go fuente el compilador generará un mensaje de error indicando la lı́nea en la que encontró el
problema y diciéndonos la causa del error. En la mayorı́a de los casos el error estará en la lı́nea
indicada, aunque puede estar en lı́neas anteriores.
Si el programa compila sin errores, podemos pasar a la siguiente fase. Si no, habrá que
volver a editar el programa fuente para corregir los errores y repetir el proceso hasta que el
compilador termine su tarea con éxito.

2.3.3. Tercer paso: Enlazado


Los programas en C usan siempre funciones de propósito general que están almacenadas
en una biblioteca. Ejemplos de estas funciones son las de impresión en pantalla, lectura del
teclado, matemáticas. . . Ahora bien, si hacemos uso de una de estas funciones es necesario
incluirla en nuestro programa final. De esto se encarga un tercer programa llamado enlazador
(linker en inglés), que busca en el código objeto las referencias a funciones que llamamos pero
que no hemos realizado nosotros, las localiza en las bibliotecas de funciones y las enlaza con
nuestro programa. El resultado final es un programa ejecutable que contiene todo el código
necesario para que el procesador realice lo que le hemos indicado en nuestro programa fuente.
Una vez realizado este paso podemos ejecutar el programa y comprobar si lo que hace es
lo que realmente queremos. Si no es ası́, se habrá producido lo que se denomina un error de
ejecución y habrá que volver al primer paso para corregir nuestro programa fuente y repe-
tir el proceso: edición, compilación, enlace y ejecución, hasta que el programa haga lo que
realmente queremos.

2.4. Nuestro primer programa en C


Una vez descrito todo el proceso vamos a realizar nuestro primer programa en C. El pro-
grama es muy simple: se limita a escribir un mensaje en la pantalla del ordenador. A pesar de
esto contiene la mayorı́a de los elementos del lenguaje. El programa es:

/* Programa: Hola
*
* Descripción: Escribe un mensaje en la pantalla del ordenador
*
* Revisión 0.0: 16/02/1998
*
* Autor: José Daniel Muñoz Frı́as
*/

#include <stdio.h> /* Declara las funciones de entrada-salida estándar */

main(void)
{
printf("Hola!\n"); /* Imprimo el mensaje */
}

Veamos a continuación cada una de las partes que componen el programa:


20 CAPÍTULO 2. EL PRIMER PROGRAMA EN C

2.4.1. Comentarios
En primer lugar vemos las lı́neas:
/* Programa: Hola
*
* Descripción: Escribe un mensaje en la pantalla del ordenador
*
* Revisión 0.0: 16/02/1998
*
* Autor: José Daniel Muñoz Frı́as
*/
que forman la ficha del programa. La finalidad de esta ficha es la documentación del programa,
de forma que cualquier persona sepa el nombre, la finalidad, la revisión y el autor del programa
sin más que leer el principio del archivo del código fuente.
Si observamos más detenidamente las lı́neas anteriores veremos que comienzan por /*
y terminan por */ (los demás * se han colocado con fines decorativos). En C todo el texto
encerrado entre /* y */ se denomina comentario y es ignorado por el compilador, de forma
que el programador pueda escribir lo que quiera con fines de documentación del código. Esto,
que puede parecer una tonterı́a en un programa tan simple como el mostrado en esta sección,
es fundamental cuando se abordan programas más grandes.
Existen dos tipos de comentarios2: los que acabamos de ver, tipo ficha del programa, y los
que se insertan en el código para aclarar operaciones que no sean obvias. Ejemplos de este
tipo de comentarios son:
#include <stdio.h> /* Declara las funciones de entrada-salida estándar */

printf("Hola!\n"); /* Imprimo el mensaje */


En este caso, al ser el primer programa que realizamos, los comentarios incluidos son
obvios, habiéndose añadido simplemente con fines ilustrativos.

2.4.2. Directivas del preprocesador


Todas las lı́neas que comienzan por el carácter # son directivas del preprocesador de C.
Este preprocesador es una parte del compilador que se encarga de realizar varias tareas para
preparar nuestro archivo de código fuente antes de realizar el proceso de compilación. Las
directivas del preprocesador le dan instrucciones a éste para que realice algún tipo de proceso.
Ası́ en la lı́nea:
#include <stdio.h> /* Declara las funciones de entrada-salida estándar */
se le dice al preprocesador que incluya el fichero cabecera stdio.h. Este fichero ya ha si-
do creado por el programador del compilador, aunque ya veremos más adelante que tam-
bién nosotros podemos incluir nuestros propios ficheros cabecera. El proceso de inclusión de
ficheros realizado por el preprocesador consiste en sustituir la lı́nea:
#include <stdio.h>
por el contenido del fichero stdio.h. Ası́, si suponemos que el contenido del fichero stdio.h
es:
2 Desde el punto de vista del programador, pues para el compilador todos son iguales.
2.4. NUESTRO PRIMER PROGRAMA EN C 21

/* Este
es
el
fichero
stdio.h*/

el código fuente después de pasar por el preprocesador queda como:

/* Programa: Hola
*
* Descripción: Escribe un mensaje en la pantalla del ordenador
*
* Revisión 0.0: 16/02/1998
*
* Autor: José Daniel Muñoz Frı́as
*/

/* Este
es
el
fichero
stdio.h*/ /* Declara las funciones de entrada-salida estándar */

main(void)
{
printf("Hola!\n"); /* Imprimo el mensaje */
}

La utilidad de incluir ficheros es la de poder escribir en un fichero declaraciones de fun-


ciones, de estructuras de datos. . . que sean usadas repetidamente por nuestros programas, de
forma que no tengamos que escribir cada vez que realizamos un nuevo programa dichas
declaraciones. En este ejemplo, antes de poder usar la función printf(), es necesario de-
cirle al compilador que esa función existe en una biblioteca, y que no debe preocuparse si
no está en nuestro archivo de código fuente, pues ya se encargará el enlazador de añadirla al
programa ejecutable.
Existen más directivas del preprocesador que se irán estudiando a lo largo del curso.

2.4.3. La función principal


Todo programa en C esta constituido por una o más funciones. Cuando se ejecuta un pro-
grama, éste ha de empezar siempre por un lugar predeterminado. En BASIC por ejemplo el
programa comienza a ejecutarse por la lı́nea 1. En C en cambio, para dotarlo de mayor flex-
ibilidad, la ejecución arranca desde el comienzo de la función main(). Por tanto, en nuestro
programa vemos que después de incluir los archivos de cabecera necesarios, aparece la lı́nea:

main(void)

que indica que el bloque que sigue a continuación es la definición de la función principal.
Este bloque está entre { y } y dentro están las instrucciones de nuestro programa, que en este
ejemplo sencillo es sólo una llamada a una función del sistema para imprimir por pantalla un
mensaje.
22 CAPÍTULO 2. EL PRIMER PROGRAMA EN C

2.4.4. Las funciones


La realización de un programa complejo requiere la división del problema a resolver en
partes más pequeñas hasta que se llega a un nivel de complejidad que puede programarse en
unas pocas lı́neas de código. Esta metodologı́a de diseño se denomina Arriba-Abajo, refi-
namientos sucesivos o Top-Down en inglés.
Para permitir este tipo de desarrollo el lenguaje C permite la descomposición del programa
en módulos a los que se denominan funciones. Estas funciones permiten agrupar las instruc-
ciones necesarias para la resolución de un problema determinado. El uso de funciones tiene
dos ventajas. La primera es que permite que unos programadores usen funciones desarrolladas
por otros. La segunda es que mejora la legibilidad del código al dividirse un programa grande
en funciones de pequeño tamaño que permiten concentrarse sólo en una parte del código total.
En esta primera parte del curso sólo se van a usar funciones ya escritas por otros progra-
madores, dejándose para la parte final la el manejo y creación de funciones.
Para usar una función ya creada sólo hacen falta dos cosas:
Incluir el fichero cabecera donde se declara la función a usar.
Realizar la llamada a la función con los argumentos apropiados.
La primera parte ya se ha visto como se realiza. La segunda requiere simplemente escribir
el nombre de la función, seguida por sus argumentos encerrados entre paréntesis. Ası́ para
llamar a la función printf() en el programa se hace:
printf("Hola!\n"); /* Imprimo el mensaje */
En el que se ha pasado el argumento "Hola!\n" a la función. Este argumento es una
cadena de caracteres, que es lo que escribirá la función printf() en la pantalla. En realidad
lo que se imprime es Hola!, pues los caracteres \n forman lo que se denomina una secuencia
de escape. Las secuencias de escape son secuencias de dos o más caracteres que representan
caracteres que no son imprimibles. En este caso la secuencia \n significa que se avance una
lı́nea. Existen más secuencias de escape que se estudiarán más adelante.
Otro detalle que se aprecia en la lı́nea de código anterior es que al final se ha puesto un ;.
Este carácter indica el final de cada instrucción de C y ha de ponerse obligatoriamente. Esto
presenta como inconveniente el que a los programadores noveles se les olvida con frecuencia,
con el consiguiente lı́o del compilador y ristra de mensajes de error, a menudo sin sentido para
el pobre novatillo. La principal ventaja es que ası́ una instrucción puede ocupar más de una
lı́nea sin ningún problema, como por ejemplo:
resultado_de_la_operacion = variable_1 + variable_2 + variable_muy_chula
+ la_ultima_variable_que_hay_que_sumar;

2.4.5. Finalización del programa


El programa finaliza su ejecución cuanto termina de ejecutarse la función main(), lo cual
ocurre cuando se llega a la llave que cierra el bloque de la función (}).

2.4.6. La importancia de la estructura


El mismo programa anterior se podı́a haber escrito de la siguiente manera:
#include <stdio.h>
main(void){printf("Hola!\n");}
2.4. NUESTRO PRIMER PROGRAMA EN C 23

Este programa desde el punto de vista del compilador es idéntico al programa original,
es decir, el compilador generará el mismo programa ejecutable que antes. El inconveniente de
este “estilo” de programación es que el código fuente es bastante más difı́cil de leer y entender.
Por tanto, aunque al compilador le da lo mismo la estructura que posea nuestro código
fuente, a los pobres lectores de nuestra obra sı́ que les interesa. Por tanto a lo largo del curso, y
especialmente en el laboratorio, se penalizará enormemente a aquellos alumnos rebeldes que
no sigan las normas de estilo de un buen programador en C, que se resumen en:
Escribir al principio de cada programa un comentario que incluya el nombre del progra-
ma, describa la tarea que realiza, indique la revisión, fecha y el nombre del programador.
Cada instrucción ha de estar en una lı́nea separada.
Las instrucciones pertenecientes a un bloque han de estar sangradas respecto a las llaves
que lo delimitan:

{
printf("Hola!\n");
}

Hay que comentar todos los aspectos importantes del código escrito.
24 CAPÍTULO 2. EL PRIMER PROGRAMA EN C
Capı́tulo 3

Tipos de datos

3.1. Introducción
Este capı́tulo introduce las variables y los tipos de datos básicos de C (int, float, double
y char). También se explican las funciones básicas de entrada y salida que van a permitir leer
desde teclado y escribir por pantalla variables de cualquier tipo. Tanto las variables como las
funciones de entrada y salida se utilizan en todos los programas.

3.2. Variables
Todos los datos que maneja un programa se almacenan en variables. Estas variables tienen
un nombre arbitrario definido por el programador e invisible para el usuario del programa.
Ni el resultado, ni la velocidad de cálculo ni la cantidad de memoria utilizada dependen del
nombre de las variables. Algunos ejemplos son x, i, variable temporal, Resistencia2. . .
Sin embargo hay que tener en cuenta que existen algunas restricciones en el nombre de las
variables:

Los nombres pueden contener letras ’a’..’z’, ’A’..’Z’, dı́gitos ’0’..’9’ y el carácter
’ ’; sin embargo siempre deben comenzar por una letra. Otros caracteres de la tabla
ASCII como por ejemplo el guión ’-’, el punto ’.’, el espacio ’ ’, el dólar ’$’ . . . no
son válidos.

Algunos nombres de variables no son válidos por ser palabras reservadas del lenguaje,
por ejemplo if, else, for, while, etc.

El compilador dará un error si se intentan utilizar nombres de variables no válidos.


Es importante recordar que C es un lenguaje de programación que distingue minús-culas y
mayúsculas y por lo tanto la variable X1 es diferente a la variable x1. Generalmente en C todos
los nombres de variables se definen en minúsculas y en los nombres formados por varias pal-
abras se utiliza el carácter ’ ’ para separarlas. Por ejemplo suele escribirse variable temporal
en lugar de variabletemporal, que resulta más confuso de leer para el propio programador
y para otros programadores que revisen su código.

25
26 CAPÍTULO 3. TIPOS DE DATOS

3.3. Tipos básicos de variables


El lenguaje C proporciona cuatro tipos básicos de variables para manejar números enteros,
números reales y caracteres. Otros tipos de variables se definen a partir de estos tipos básicos.

3.3.1. Números enteros


Los números enteros, positivos o negativos, se almacenan en variables de tipo int. La
manera de declarar una variable llamada mi primer entero como entera es escribir la sigu-
iente lı́nea:

int mi_primer_entero;

Una vez declarada la variable se puede almacenar en ella cualquier valor entero que se en-
cuentre dentro del rango del tipo int. Por ejemplo, en la variable mi primer entero se puede
almacenar el valor 123, pero no el valor 54196412753817103 por ser demasiado grande ni 7.4
por ser un número real. El rango de valores que pueden almacenarse en variables de tipo int
está relacionado con el número de bytes que el ordenador utilice para este tipo de variables,
según se ha visto en la sección 1.3. Al definir una variable de tipo int, el compilador se encar-
ga de reservar unas posiciones de la memoria del ordenador donde se almacenará el valor de
la variable. Inicialmente el contenido de estas posiciones de memoria es desconocido 1, hasta
que se asigna valor a la variable, por ejemplo:

int i;

i=7;

Al hacer la asignación, el valor 7 se convierte a ceros y unos (de acuerdo a la codificación de


los números enteros) y el resultado se almacena en las posiciones de memoria reservadas para
la variable i.

3.3.2. Números reales


Los números reales se almacenan en variables de tipo float o de tipo double. La manera
de declarar variables de estos tipos es análoga al caso de variables enteras:

float x;
double z;

La única diferencia entre float y double es que la primera utiliza menos cantidad de memo-
ria; como consecuencia tiene menor precisión y menor rango 2 pero utiliza menos memoria.
Como se vio en el capı́tulo de introducción, las variables reales se codifican en forma de una
mantisa, donde se almacenan las cifras significativas, y un exponente. Ambos pueden ser pos-
itivos o negativos.

1 Unerror muy común en los programadores novatos es suponer que las variables no inicializadas valen cero.
2
El rango de una variable está relacionado con los valores máximo y mı́nimo que puede almacenar mientras que
la precisión está relacionada con el número de cifras significativas (o número de decimales). Por ejemplo el rango de
las variables float suele ser del orden de 1E+38 y la precisión de unas 6 cifras significativas.
3.4. MÁS TIPOS DE VARIABLES 27

3.3.3. Variables de carácter


Un carácter es una letra, un dı́gito, un signo de puntuación o en general cualquier sı́mbolo
que puede escribirse en pantalla o en una impresora. El lenguaje C sólo tiene un tipo básico
para manejar caracteres, el tipo char, que permite definir variables que almacenan un carácter.
Ya se verá más adelante que para almacenar cadenas de caracteres, por ejemplo nombres y
direcciones, hay que trabajar con conjuntos de variables tipo char (vectores de char). La
manera de declarar la variable c como tipo char es la siguiente:
char c;
Hay que tener en cuenta que los caracteres se codifican en memoria de una manera un poco
especial. Como se vio en el capı́tulo 1, la tabla ASCII asocia un número de orden a cada
carácter, y cuando se guarda una letra en una variable tipo char, el compilador almacena
el número de orden en la posición de memoria reservada para la variable. Por ejemplo si
se escribe c=’z’; se almacenará el valor 122 en la posición de memoria reservada para la
variable c; es decir se almacenará 01111010.

Ejercicio
1. ¿Es igual hacer c=’9’; que c=9;? ¿Que valor se almacenará en la posición de memoria
reservada para la variable c en cada caso?

3.4. Más tipos de variables


Existen unos modificadores que permiten definir más tipos de variables a partir de los tipos
básicos. Estos modificadores son short (corto), long (largo) y unsigned (sin signo), que
pueden combinarse con los tipos básicos de distintas maneras. Los nuevos tipos de variables
a los que dan lugar son los siguientes (algunos pueden abreviarse):
short int = short
long int = long
long double
unsigned int = unsigned
unsigned short int = unsigned short
unsigned long int = unsigned long
unsigned char
En general los modificadores short y long cambian el tamaño de la variable básica y por
lo tanto cambian el rango de valores que pueden almacenar. Por otro lado el modificador
unsigned, que no puede aplicarse a los tipos reales, modifica el rango de las variables sin
cambiar el espacio que ocupan en memoria. Por ejemplo, en un ordenador donde el tipo int
puede almacenar valores en el rango desde -32768 hasta 32767, una variable unsigned int
tendrı́a un rango desde 0 hasta 65535, ya que no utiliza números negativos. En ambos casos
el número de valores diferentes que pueden tomar las variables es el mismo (65536 valores
posibles = 216 ).

3.5. Constantes
Las constantes son, en general, cualquier número que se escribe en el código y que por
lo tanto no puede modificarse una vez que el programa ha sido compilado y enlazado. Si
28 CAPÍTULO 3. TIPOS DE DATOS

escribimos por ejemplo i=7, la variable i tomará siempre el valor 7, ya que este número es fijo
en el programa: es una constante.
El compilador considera como constantes de tipo int todos los números enteros, posi-
tivos o negativos, que se escriben en un programa. Para que estas constantes se consideren de
tipo long deben llevar al final una L o una l, para que se consideren de tipo unsigned deben
llevar una u o una U y los enteros largos sin signo debe llevar ul o bien UL. Ver el siguiente
ejemplo:
int entero;
unsigned sin_signo;
long largo;
unsigned long sin_signo_largo;

entero=7;
sin_signo=55789u;
largo=-300123456L; /*es mejor L, porque l se confunde con un 1 */
sin_signo_largo=2000000000UL;
Las constantes de tipo entero también pueden escribirse en octal y en hexadecimal, lo que
nos ahorra tener que hacer las conversiones a decimal mientras escribimos el programa. Los
números en base 8 se escriben empezando con cero y en base 16 empezando por 0x o por 0X.
Las tres lı́neas siguientes son equivalentes:
i=31; /*Decimal*/
i=037; /*Octal*/
i=0x1F; /*Hexadecimal, también vale poner 0x1f*/
Los números reales se identifican porque llevan un punto (123.4) o un exponente (1e-2)
o ambas cosas. En estos casos la constante se considera de tipo double.
Las constantes de caracteres se escriben entre comillas simples como en el siguiente
ejemplo:
char c;

c=’z’;
En realidad las constantes de caracteres se convierten automáticamente a tipo int de acuerdo
a la tabla ASCII, o sea que la instrucción anterior es equivalente a escribir:
char c;

c=122; /* Esta asignación es correcta en C,


aunque c es de tipo char, no int */
Determinados caracteres especiales se pueden escribir utilizando secuencias de escape que
comienzan por el carácter \, como \n en el programa del capı́tulo 2. Los ejemplos más tı́picos
son los siguientes:
’\n’ cambio de lı́nea (newline)
’\r’ retorno de carro (return)
’\0’ carácter nulo (NULL). No confundir con el carácter ’0’.
’\t’ TAB
’\f’ cambio de página (form feed)
3.6. TAMAÑO DE LAS VARIABLES 29

’\’’ comilla simple


’\"’ comilla doble
’\\’ la barra \

3.6. Tamaño de las variables


El tamaño de cada tipo de variable no está fijado por el lenguaje, sino que depende del
tipo del ordenador y a veces del tipo de compilador que se utilice. Todo programa que necesite
conocer el tamaño de una determinada variable puede utilizar el operador sizeof para obten-
erlo, pero es una mala técnica de programación suponer un tamaño fijo. El operador sizeof
puede utilizarse tanto con tipos concretos (sizeof(int)) como con variables (sizeof(i)).
El operador sizeof devuelve un entero que es el número de bytes que ocupa la variable en
memoria.
El tipo short ocupa al menos 2 bytes en todos los compiladores y long ocupa un mı́nimo
de 4, mientras que int suele ser de tamaño 2 ó 4 dependiendo del tipo de ordenador (mı́nimo
2). En definitiva, lo que ocurrirá en cualquier implantación del lenguaje es que: sizeof(short
int) sizeof(int) sizeof(long int).

3.7. Escritura de variables en pantalla


La función más utilizada para mostrar el valor de una variable por pantalla es la función
printf(), que forma parte de la biblioteca estándar de funciones de entrada y salida en C.
Esta función está declarada en el archivo de cabeceras (header file) stdio.h, por lo tanto es
necesario incluir este archivo en el programa para que el compilador pueda hacer las compro-
baciones oportunas sobre la sintaxis de la llamada a printf. Sin embargo, al ser una función
de la biblioteca estándar, no es necesario incluir ninguna biblioteca (library) especial para
generar el ejecutable, ya que dicha librerı́a se enlaza por defecto.
La función printf permite escribir cualquier texto por pantalla, como se mostró en el
programa hola.c del capı́tulo anterior. Pero también se puede utilizar para mostrar el valor
de cualquier variable y además nos permite especificar el formato de salida. Los valores de las
variables pueden aparecer mezclados con el texto, por lo tanto es necesario definir una cadena
de caracteres especial donde quede clara la posición en la que deben mostrarse los valores de
las variables. Las posiciones donde aparecen las variables se definen utilizando el carácter %
seguido de una letra que indica el tipo de la variable. Ejemplo:
/*********************
Programa: Entero.c
Descripción: Escribe un número entero
Revisión 0.1: 16/FEB/1999
Autor: Rafael Palacios
*********************/

#include <stdio.h>

main(void)
{
int dia;

dia=47;
30 CAPÍTULO 3. TIPOS DE DATOS

printf("Hoy es el dı́a %d del año\n",dia);


}

En el programa anterior la función printf sustituye el texto %d por el valor de la variable dia
expresada como un entero. Este programa escribe por pantalla la siguiente lı́nea:

Hoy es el dı́a 47 del año

Cada tipo de variable requiere utilizar una letra diferente después del carácter %, los formatos
más normales para los tipos básicos de variables se muestran en la siguiente tabla:

formato tipo de variable


%d int
%f float y double
%c char

La función printf puede escribir varias variables a la vez, del mismo tipo o de tipo distin-
to, pero hay que tener cuidado para incluir en la cadena de caracteres tantos % como número de
variables. Además hay que tener cuidado para introducir correctamente los formatos de cada
tipo de variable. Ejemplo:

/*********************
Programa: Tipos.c
Descripción: Escribe variables de distintos tipos
Revisión 0.1: 16/FEB/1999
Autor: Rafael Palacios
*********************/

#include <stdio.h>

main(void)
{
int dia;
double temp;
char unidad;

dia=47;
temp=11.4;
unidad=’C’;
printf("Dı́a %d, Temperatura %f %c\n",dia,temp,unidad);
}

--------------- Salida --------------------------


Dı́a 47, Temperatura 11.400000 C

3.7.1. Escritura de variables de tipo entero


Las variables de tipo entero pueden escribirse en varios formatos: decimal, octal o hex-
adecimal. La función printf no proporciona un formato para escribir el valor en binario.
3.7. ESCRITURA DE VARIABLES EN PANTALLA 31

Los formatos para variables de tipo entero son %d para mostrar el valor en decimal, %o para
mostrarlo en octal y %X ó %x para mostrarlo en hexadecimal3. En formato octal y hexadecimal
se muestra el valor sin signo.

printf("Decimal %d, en octal %o, en hexadecimal %X\n",dia,dia,dia);

escribe la siguiente lı́nea en pantalla:

Decimal 47, en octal 57, en hexadecimal 2F

Los tipos de variables derivados de int, como short, long, unsigned. . . utilizan los carac-
teres de formato que se muestran en la siguiente tabla:

formato tipo de variable


%d int
%hd short
%ld long

%u unsigned int
%hu unsigned short
%lu unsigned long

3.7.2. Escritura de variables de tipo real


Las variables de tipo real, tanto float como double se escriben con formato %f, %e, %E, %g
ó %G, dependiendo del aspecto que se quiera obtener. Con %f siempre se escribe el punto deci-
mal y no se escribe exponente, mientras que con %e siempre se escribe una e para el exponente
y con %E se escribe una E. Por ejemplo:

/*********************
Programa: Formatos.c
Descripción: Escribe números reales en distintos formatos
Revisión 0.0: 16/FEB/1999
Autor: Rafael Palacios
*********************/

#include <stdio.h>

main(void)
{
printf("%f\n",12345.6789);
printf("%e\n",12345.6789);
printf("%E\n",12345.6789);
}

--------------- Salida --------------------------


12345.678900
1.234568e+04
1.234568E+04
3 La única diferencia entre %X y %x es que con el primer formato los sı́mbolos de la A a la F del número en base

hexadecimal se escriben en mayúsculas mientras que con el segundo se escriben en minúsculas


32 CAPÍTULO 3. TIPOS DE DATOS

El formato %g es el más seguro cuando no se conoce de qué orden es el valor a escribir ya


que elige automáticamente entre %f (que es más bonito) y %e (donde cabe cualquier número
por grande que sea).

3.7.3. Escritura de variables de tipo carácter


Las variables tipo char se escriben con formato %c lo que hace que aparezca el carácter en
pantalla. También es cierto que una variable tipo char puede escribirse en formato decimal,
es decir con %d, ya que los tipos char e int son compatibles en C. En caso de escribir una
variable tipo char con formato %d se obtiene el número de orden correspondiente en la tabla
ASCII. Comprobad el siguiente programa viendo la tabla ASCII.
/*********************
* Programa: Caracter.c
* Descripción: Escribe variables de tipo char
* Revisión 0.0: 16/FEB/1999
* Autor: Rafael Palacios
*********************/

#include <stdio.h>

main(void)
{
char c;

c=’z’;
printf("Carácter %c\n",c);
printf("Valor %d\n",c);
}

--------------- Salida --------------------------


Carácter z
Valor 122

3.7.4. Escritura de variables con formato


La función printf también permite definir el tamaño con el que aparece cada variable
en la pantalla y la alineación, derecha o izquierda, que debe tener. Esto se hace escribiendo
números entre el carácter % y el carácter que define el tipo de variable. En lugar de enumerar
las reglas que se utilizan para definir estos formatos, lo más práctico es mostrar una lista de
ejemplos que se puede utilizar como referencia rápida con mayor comodidad.
variables enteras
printf(":%5d: \n",i); --> : 123: Hay espacio suficiente
printf(":%-5d: \n",i); --> :123 : Alineación izquierda
printf(":%05d: \n",i); --> :00123: Llena con ceros
printf(":%2d: \n",i); --> :123: No cabe, formato por defecto

variables reales
printf(":%12f: \n",x); --> : 1024.251000: Por defecto 6 decimales
printf(":%12.4f: \n",x); --> : 1024.2510: .4 indica 4 decimales
3.8. LECTURA DE VARIABLES POR TECLADO 33

printf(":%-12.4f: \n",x); --> :1024.2510 : Alineación izquierda


printf(":%12.1f: \n",x); --> : 1024.3: Redondea correctamente
printf(":%3f: \n",x); --> :1024.251000: No cabe-> por defecto
printf(":%.3f: \n",x); --> :1024.251: Tamaño por defecto con 3 dec.

printf(":%12e: \n",x); --> :1.024251e+03: 12 caracteres


printf(":%12.4e: \n",x); --> : 1.0243e+03: Idem. pero con 4 dec.
printf(":%12.1e: \n",x); --> : 1.0e+03: Idem. pero con 1 dec.
printf(":%3e: \n",x); --> :1.024251e+03: No cabe. Por defecto
printf(":%.3e: \n",x); --> :1.024e+03: 3 decimales.

3.8. Lectura de variables por teclado


La función más cómoda para lectura de variables desde teclado es la función scanf, que
tiene un formato similar a printf. También se trata de una función de la biblioteca estándar
de entrada y salida y por lo tanto funciona en cualquier compilador de C. En este apartado
sólo se verá la manera de leer variables de tipo básico aunque scanf es una función potente
que admite muchas opciones.
El siguiente programa lee una variable del teclado y escribe el valor en pantalla

/*********************
Programa: Leer.c
Descripción: Lee una variable con scanf
Revisión 0.0: 16/MAR/1998
Autor: Rafael Palacios
*********************/

#include <stdio.h>

main(void)
{
int a;

printf("Dame un valor ");


scanf("%d",&a);
printf("Has escrito %d\n",a);
}

Puede observarse que la sintaxis de scanf y de printf es similar. La función scanf recibe
una cadena de caracteres donde se define el formato de la variable a leer y luego el nombre de
la variable precedido de un signo ‘&’. Este signo ‘&’ es fundamental para que scanf funcione
correctamente. En el capı́tulo de funciones se explicará por qué es necesario. Los formatos
de los distintos tipos de variables son los mismos que para printf salvo que las variables
float se leen con formato %f y las double se leen con %lf. Para escritura ambas utilizan el
formato %f, si bien la mayorı́a de los compiladores también admiten especificar el formato
como %lf para escritura y se evita la pequeña inconsistencia que existe entre printf y scanf.
34 CAPÍTULO 3. TIPOS DE DATOS

Ejercicios
1. Escribe una tabla con todos los tipos de datos que incluya los formatos que utiliza cada
uno con printf y scanf
Capı́tulo 4

Operadores en C

4.1. Introducción
En el capı́tulo anterior se han visto los tipos de datos básicos del lenguaje C, cómo leer
estos datos desde teclado y cómo imprimirlos en la pantalla. Puesto que los ordenadores son
equipos cuya misión es el proceso de datos, los lenguajes de programación proveen al progra-
mador con una serie de operadores que permiten realizar las operaciones necesarias sobre los
datos. En este capı́tulo vamos a estudiar los distintos operadores soportados por el lenguaje C.

4.2. El operador de asignación


El operador de asignación en C se representa mediante el signo =. Este operador asigna el
valor de la expresión situada a su derecha a la variable situada a su izquierda, ası́ por ejemplo,
para asignar a la variable n el valor 2 se escribe:
n = 2;
No ha de confundirse este operador con la igualdad en sentido matemático, aunque se use
el mismo sı́mbolo. En matemáticas se puede escribir 4 2 2, cosa que darı́a un soberano


error en C al intentar compilar, pues 4 no es una variable. Tampoco es legal en C la instrucción


2 = n, aunque matemáticamente sea una expresión correcta.
En resumen el operador de asignación en C evalúa la expresión situada a su derecha y
asigna su valor a la variable situada a su izquierda:

variable = expresión;

4.3. Operadores para números reales


En la mayorı́a de las aplicaciones es necesario operar con números reales. Para hacer es-
to posible en el lenguaje C, éste soporta las operaciones matemáticas básicas, es decir, la
suma, resta, multiplicación y división. El resto de operaciones (potenciación 1, logaritmos,
funciones trigonométricas. . . ) están implantadas en una biblioteca matemática, debiendo in-
cluirse el fichero math.h si se desean usar2 .
1 Al contrario que en otros lenguajes el operador ˆ no es el de elevar un número a una potencia.
2 Además es necesario decirle al enlazador que añada la biblioteca matemática, lo cual depende del sistema de
desarrollo usado.

35
36 CAPÍTULO 4. OPERADORES EN C

Las operaciones de suma, resta, multiplicación y división se representan en C mediante


los operadores +, -, * y / respectivamente.
Supongamos que estamos desarrollando un programa de facturación y que tenemos que
calcular el importe total a partir de la base imponible para un IVA del 16 %. La forma de
realizarlo serı́a:

total = base * 1.16;

En donde se realiza el producto de la variable base por la constante 1.16 para después asignar
el resultado a la variable total.
En una misma expresión se pueden combinar varios operadores, aplicándose en este caso
las mismas reglas de precedencia usadas en matemáticas, es decir, en primer lugar se realizan
las multiplicaciones y las divisiones, seguidas de las sumas y restas. Cuando las operaciones
tienen la misma precedencia, por ejemplo 2*pi*r, éstas se realizan de izquierda a derecha.
Siguiendo con el ejemplo anterior, si tenemos dos artı́culos de los cuales el primero es un
libro, al que se le aplica un 4 % de IVA y el segundo una tableta de chocolate, a la que se le
aplica (injustamente) un IVA del 16 %, el cálculo del importe total de la factura se realizarı́a
en C mediante la siguiente lı́nea de código:

total = 1.04*ValorLibro + 1.16*ValorChocolate;

En donde en primer lugar se evalúa 1.04*ValorLibro, seguidamente 1.16*ValorChocolate


y por último se suman los resultados de ambas operaciones, asignándose el valor de la suma a
la variable total.
Si se desea alterar el orden de precedencia preestablecido, se pueden usar paréntesis al
igual que en matemáticas, eso si, sólo paréntesis; las llaves y los corchetes tienen otros signifi-
cados en el lenguaje. Siguiendo con el ejemplo anterior, si los dos artı́culos tienen el mismo
tipo de IVA, el cálculo del total de la factura se escribirı́a en C:

total = 1.16*(ValorChocolate + ValorHelado);

En este caso se realiza en primer lugar la suma de ValorChocolate a ValorHelado y el


resultado se multiplicará por la constante 1.16, asignándose el resultado a la variable total.
Si hubiésemos escrito la lı́nea anterior sin los paréntesis, es decir:

total = 1.16 * ValorChocolate + ValorHelado; /*Factura mal calculada*/

El programa multiplicarı́a 1.16 por ValorChocolate y al resultado le sumarı́a el ValorHelado,


por lo que serı́amos perseguidos duramente por el fisco por facturar helados sin IVA.

4.4. Operadores para números enteros


El lenguaje C también soporta las operaciones básicas para los números enteros. La fun-
cionalidad y los sı́mbolos usados son los mismos que los usados para los números reales. La
única diferencia está en el operador de división, que en el caso de números enteros da como
resultado la parte entera de la división. Ası́ por ejemplo 3/2 da como resultado 1, lo que puede
parecer malo, pero aún hay cosas peores como 1/2, que da como resultado 0, lo cual tiene un
gran peligro en expresiones como:

resultado = 1/2*3;
4.5. OPERADOR UNARIO - 37

Como los dos operadores tienen la misma precedencia, la expresión se evalúa de izquierda
a derecha, con lo cual en primer lugar se efectúa 1/2 con el resultado antes anunciado de 0
patatero, que al multiplicarlo después por el 3 vuelve a dar 0, como todo el mundo habrá ya
adivinado. Si en cambio se escribe:
resultado = 3*1/2;
El resultado ahora es de 1, con lo que se aprecia que en lenguaje C cuando se trabaja con
números enteros lo de la propiedad conmutativa no funciona nada bien, y deja bien claro
que, en contra de la creencia popular, los ordenadores no son infalibles realizando cálculos
matemáticos, sobre todo cuando el programador no sabe muy bien lo que está haciendo y
descuida temas como los redondeos de las variables o los rangos máximos de los tipos.

4.4.1. Operador resto


Siguiendo con la división de números enteros, existe un operador que nos da el resto de
la división (no es tan bueno como tener los decimales, pero al menos es un consuelo). El
operador resto se representa mediante el sı́mbolo %. Ası́ por ejemplo 4 %2 da como resultado
0 y 1 %2 da como resultado 1.
Una utilidad de este operador es la de averiguar si un determinado número es múltiplo de
otro; por ejemplo el número 4580169 es múltiplo de 33 porque 4580169 % 33 da cero.
Por supuesto este operador no tiene sentido con números reales, por lo que el compilador
se quejará si lo intentamos usar en ese caso.

4.4.2. Operadores de incremento y decremento


Dado que una de las aplicaciones principales de los números enteros en los programas es
la realización de contadores, que usualmente se incrementan o decrementan de uno en uno, los
diseñadores de C vieron aconsejable definir unos operadores para este tipo de operaciones 3.
El operador incremento se representa añadiendo a la variable que se desee incrementar dos
sı́mbolos +. La sintaxis es por tanto: NombreVariable++. Ası́ por ejemplo la lı́nea:
Contador++;
Sumarı́a uno a la variable Contador.
El operador decremento es idéntico al de incremento, sin mas que sustituir los + por -.
Siguiendo el ejemplo anterior:
Contador--;
Le restará uno a la variable Contador.

4.5. Operador unario -


Hasta ahora, todos los operadores que se han discutido eran binarios, es decir, operaban
con dos números: el situado a su izquierda con el situado a su derecha. El operador unario -
toma sólo un valor situado a su derecha y le cambia el signo. Ası́ en:
i = -c;
el operador - toma el valor de la variable c y le cambia el signo. Por tanto si c vale 17, al
finalizar la ejecución de la instrucción, la variable i contendrá el valor -17.
3
Además todos los procesadores tienen instrucciones especiales de incremento y decremento, usualmente más
rápidas que la de suma normal, con lo que estos operadores le permiten al compilador usar ese tipo de instrucciones,
consiguiendo un programa más eficiente.
38 CAPÍTULO 4. OPERADORES EN C

4.6. De vuelta con el operador de asignación


Ya se dijo al principio que el operador de asignación en C no debı́a confundirse con la
igualdad en sentido matemático. Un uso muy frecuente del operador de asignación en C es el
ilustrado en la instrucción:

i = i + 7;

Esto, que matemáticamente no tiene ningún sentido, en los lenguajes de programación es una
práctica muy común. El funcionamiento de la instrucción no tiene nada de especial. Tal como
se explicó en la sección 4.2 el operador de asignación evalúa en primer lugar la expresión
que hay situada a su derecha para después asignársela a la variable situada a su izquierda.
Obviamente da igual que la variable a la que le asignamos el valor intervenga en la expresión,
tal como ocurre en este caso. Ası́ pues en el ejemplo anterior, si la variable i vale 3, al evaluar
la expresión se le sumará al valor actual de i un 7, con lo que el resultado de la evaluación de
la expresión será 10, que se almacenará en la variable i.
Como este tipo de instrucciones son muy comunes en C, existe una construcción especı́fica
para realizarlas: preceder el operador = por la operación a realizar (+ en este caso). Ası́ la
instrucción anterior se puede escribir:

i += 7;

Con los demás operadores la situación es similar: t /= 2 asigna a t el resultado de la


expresión t/2.
La ventaja de este tipo de construcción queda manifiesta cuando se usa un nombre de
variable largo:

variable_de_cuyo_nombre_no_quiero_acordarme += 2;

En donde, aparte del ahorro en el desgaste del teclado, nos ahorramos la posibilidad de escribir
mal la variable la segunda vez, y facilitamos la lectura a otros programadores, que no tienen
que comprobar que las variables a ambos lados del = son la misma.

4.7. Conversiones de tipos


En las expresiones se pueden mezclar variables de distintos tipos, es decir, un operador
binario puede tener a su izquierda un operando de un tipo, como por ejemplo int y a su
derecha un operando de otro tipo, como por ejemplo double. En estos casos el compilador
automáticamente se encargará de realizar las conversiones de tipos adecuadas. Estas conver-
siones trabajan siempre promoviendo el tipo “inferior” al tipo “superior”, obteniéndose un
resultado que es del tipo “superior”. De esta forma las operaciones se realizan siempre con
la precisión adecuada. Por ejemplo si i es una variable de tipo int y d es de tipo double, la
expresión i*d convierte en primer lugar el valor de i a tipo double y luego multiplica el valor
convertido por d. El resultado obtenido de la operación es también de tipo double.
Las reglas de conversión se resumen en:

Si cualquier operando es de tipo long double


Se convierte el otro operando a long double
Si no: Si cualquier operando es de tipo double
Se convierte el otro operando a double
Si no: Si cualquier operando es de tipo float
4.7. CONVERSIONES DE TIPOS 39

Se convierte el otro operando a float


Si no: Si cualquier operando es de tipo long
Se convierte el otro operando a long
Si no:
Se convierten char y short a int

La última lı́nea quiere decir que aunque los operandos de una operación sean los dos de
tipo char o short, el compilador los promociona automáticamente a int, para realizar la
operación con mayor precisión.

Cuidado con la división y los enteros


En una expresión como la siguiente:

resultado = 1/2*d;

como todos los operadores tiene la misma precedencia, la expresión se evalúa de izquierda a
derecha, realizándose en primer lugar la operación 1/2, en la que como los dos operandos son
dos constantes de tipo int, se realiza la división entera, obteniéndose un 0 como resultado. La
siguiente operación a realizar será la multiplicación que, si d es de tipo double, convertirá el
resultado anterior (0) a double y luego lo multiplicará por d, obteniéndose un resultado nada
deseado. Si se realiza la operación al revés, es decir, d*1/2 el resultado será ahora correcto
¿por qué?
Para evitar este tipo de situaciones conviene evitar usar constantes enteras cuando trabaje-
mos en expresiones con valores reales. Ası́ la mejor manera de evitar el problema anterior es
escribir:

resultado = 1.0/2.0*d

Ahora bien, este método no es válido cuando tenemos que usar en una expresión variables
enteras junto con variables reales. En este caso puede ser necesario forzar una conversión que
el compilador no realizarı́a de modo automático. Por ejemplo, si las variables i1 e i2 son de
tipo int y la variable d es de tipo double, en la expresión i1/i2*d se realizará la división de
números enteros, con el consabido peligro y falta de precisión. Para conseguir que la división
se realice con números de tipo double podemos reordenar las operaciones como se hizo antes,
o mejor, forzar al compilador a que realice la conversión. Para forzar la conversión se usa el
operador unario molde, llamado cast en inglés, que tiene el formato:
(tipo al que se desea convertir) variable a convertir
El ejemplo anterior forzando las conversiones quedarı́a como:

resultado = (double) i1 / (double) i2 * d;

Como el alumno aventajado habrá notado sólo serı́a necesario poner un molde en cualquiera
de las dos variables enteras ¿Por qué?

4.7.1. El operador asignación y las conversiones


Si el resultado de una expresión no es del tipo de la variable a la que es asignado, el
compilador realizará automáticamente la conversión necesaria. En caso de que el tipo de la
variable sea “superior” al de la expresión no existirá ningún problema, pero si es inferior, se
pueden producir pérdidas de precisión (por ejemplo al convertir de double a int se pierde la
40 CAPÍTULO 4. OPERADORES EN C

parte decimal), y en algunos casos resultados erróneos (por ejemplo al asignar un valor grande
de tipo long a un int).
Como resumen a esta sección cabe decir que se ha de ser extremadamente cuidadoso/a
cuando en una instrucción se mezclen variables y constantes de tipos distintos si se desean
evitar resultados erróneos, que a menudo sólo se dan en ciertas situaciones, funcionando el
programa correctamente en las demás.

4.8. Ejemplos
Para afianzar los conceptos vamos a estudiar a continuación algunos ejemplos de expre-
siones matemáticas en C.

main(void)
{
int ia, ib;
int ires;
double da, db;
double dres;

ia = 1;
ib = 3;
da = 2.3;
db = 3.7;

ires = ia + (ib*2 + ia*3); /* Ej 1 */


dres = ia + (ib*2 + ia*3); /* Ej 2 */
dres = ia + da*(ib/2.0 + ia/2.0); /* Ej 3*/
ires = ia + da*(ib/2.0 + ia/2.0); /* Ej 4*/
dres = -da*(ia + ib*(da + db/3.0)); /* Ej 5*/
}

En el primer ejemplo, se evalúa en primer lugar la expresión que está entre paréntesis.
Como esta expresión contiene multiplicaciones y sumas, en primer lugar se realizan las multi-
plicaciones, sumándose seguidamente los resultados. Por último al resultado se le suma ia y se
almacena el número obtenido en la variable ires. Como todas las variables involucradas son
de tipo int no se realiza ninguna conversión de tipos. Nótese que en este caso los paréntesis
son innecesarios.
El segundo ejemplo es idéntico al primero, salvo que el resultado, que recordemos es de
tipo int, se almacena en una variable de tipo double. Se realizará por tanto una conversión
de tipos del resultado de int a double antes de realizar la asignación, lo que no debe de dar
ningún problema.
En el tercer ejemplo se realizan en primer lugar las divisiones de dentro del paréntesis,
en las que debido a que el segundo operando es una constante real, los operandos enteros se
convierten a double, obteniéndose por tanto un resultado de tipo double que se multiplica
por da para sumarlo después a ia, que al ser un int ha de convertirse previamente a dou-
ble. El resultado se almacena en una variable de tipo double, por lo que no se realizan más
conversiones de tipos.
En el cuarto ejemplo el proceso es el descrito para el ejemplo anterior, salvo que al asignar
el número de tipo double que resulta a uno de tipo int, se realiza una conversión de double
4.8. EJEMPLOS 41

a int, por lo que se almacena en ires la parte entera del resultado de la expresión, siempre y
cuanto esta esté dentro el rango del tipo int, perdiéndose para siempre la parte decimal.
Por último en el ejemplo quinto se muestra la posibilidad de anidar varias expresiones
entre paréntesis. En este caso se evalúa en primer lugar el paréntesis interno, el resultado se
multiplica por ib y después se suma ia al resultado anterior. Todo ello se multiplica por da
cambiado de signo. ¿Que conversiones de tipos se han realizado en este ejemplo?

Ejercicios
1. Calcular el valor que resulta de cada una de las expresiones del programa anterior.
42 CAPÍTULO 4. OPERADORES EN C
Capı́tulo 5

Control de flujo: for

5.1. Introducción
Es habitual que los programas realicen tareas repetitivas o iteraciones (repetir las mismas
operaciones pero cambiando ligeramente los datos). Esto no supone ningún problema para
el programador novato, que después de aprender a cortar y pegar puede repetir varias veces
el mismo código, pero dentro de un lı́mite. Por ejemplo, un programa que escriba 3 veces la
dirección de la universidad para imprimir unos sobres serı́a el siguiente:

/*********************
Programa: Sobres.c
Descripción: Escribe la dirección de la Universidad en tres sobres.
Revisión 0.0: 10/MAR/1998
Autor: Rafael Palacios
*********************/

#include <stdio.h>

main(void)
{
printf("Universidad Pontificia Comillas\n");
printf("c/Alberto Aguilera 23\n");
printf("E-28015 Madrid\f");

printf("Universidad Pontificia Comillas\n");


printf("c/Alberto Aguilera 23\n");
printf("E-28015 Madrid\f");

printf("Universidad Pontificia Comillas\n");


printf("c/Alberto Aguilera 23\n");
printf("E-28015 Madrid\f");
}

Este programa es fácil de escribir, a base de copy y paste, cuando sólo se quieren imprim-
ir tres sobres, pero ¿qué ocurre si queremos imprimir sobres para todos los alumnos de la
universidad?

43
44 CAPÍTULO 5. CONTROL DE FLUJO: FOR

Todos los programas que se han visto hasta ahora tienen un flujo continuo desde arriba
hasta abajo, es decir las instrucciones se ejecutan empezando en la primera lı́nea y descendi-
endo hasta la última. Esto obliga a copiar parte del código cuando se quiere que el programa
repita una determinada tarea. Sin embargo el lenguaje C, como cualquier otro lenguaje de pro-
gramación, proporciona maneras de modificar el flujo del programa permitiendo pasar varias
veces por un conjunto de instrucciones.
Se llama bucle de un programa a un conjunto de instrucciones que se repite varias veces.
Es decir, es una zona del programa en la que el flujo deja de ser descendente y vuelve hacia
atrás para repetir la ejecución de una serie de instrucciones.
El bucle más sencillo, que se introduce en este capı́tulo, es el bucle for, que generalmente
se utiliza para repetir parte del código un cierto número de veces.

5.2. Sintaxis del bucle for


El bucle for queda definido por tres argumentos: sentencia inicial, condici ón de salida y
sentencia final de bucle. Estos argumentos se escriben separados por punto y coma y no por
coma como en las funciones.

for(sentencia inicial ; condición ; sentencia final ){


instrucción 1;
instrucción 2;
...
instrucción n;
}

Lo mejor es ver un ejemplo:

1 #include <stdio.h>
2
3 main(void)
4 {
5 int i;
6
7 for(i=0; i<3; i++) {
8 printf("Universidad Pontificia Comillas\n");
9 printf("c/Alberto Aguilera 23\n");
10 printf("E-28015 Madrid\f");
11 }
12 }

Este programa hace lo mismo que el programa anterior, pero podemos escribir el número de
sobres que queramos simplemente cambiando el 3 de la lı́nea 8 por otro número. En este
ejemplo la instrucción i=0 es la sentencia inicial, que se ejecuta antes de entrar en el bucle. La
condición es i<3, que indica que el bucle se va a repetir mientras sea cierto que la variable i
tiene un valor menor que 3. Como inicialmente se asigna el valor 0 a la variable i, inicialmente
se cumple la condición del bucle, por lo tanto el flujo del programa entra en el bucle y se
ejecutan todas las sentencias comprendidas entre las dos llaves ( { en la lı́nea 8, y } en la lı́nea
12). Al alcanzar la lı́nea 12 se ejecuta la sentencia final de bucle, en este caso i++ y luego se
vuelve a comprobar la condición. En la primera iteración la variable i vale 0, pero al llegar al
final del bucle la instrucción i++ hace que pase a valer 1, entonces se vuelve a comprobar la
5.2. SINTAXIS DEL BUCLE FOR 45

condición i<3 que se cumple y por lo tanto el flujo vuelve a la lı́nea 9. Este bucle se ejecuta 3
veces en total, durante la primera pasada i vale 0, al llegar a la lı́nea 12 pasa a valer 1 y hace
la segunda pasada, entonces pasa a valer 2 y hace la tercera. Al terminar la tercera pasada, la
instrucción i++ hace que i valga 3 y por lo tanto deja de cumplirse la condición i<3 ya que 3
no es menor que 3. Como puede apreciarse, la variable i controla el número de veces que se
ejecuta el bucle. Por ello a este tipo de variables se les denomina variables de control.
Es muy importante hacer un buen uso del sangrado para facilitar la lectura del programa.
En este ejemplo puede observarse que todas las instrucciones del programa tienen un sangrado
de tres espacios, pero las instrucciones del bucle (lı́neas 9 a 11) tienen un sangrado adicional de
otros tres espacios. De esta manera se ve claramente que la llave de la lı́nea 12 cierra el bucle
for de la lı́nea 8 mientras que la llave de la lı́nea 14 cierra la función main haciendo pareja
con la llave de la lı́nea 5. Puesto que el compilador ignora todos los espacios y los cambios de
lı́nea, cada programador puede elegir su estilo propio, lo importante es ser coherente en todo
el código. Por ejemplo en lugar de 3 espacios pueden escribirse siempre 2, lo que no vale es
poner unas veces 3 y otras 2 porque entonces las columnas salen hechas un churro. Tampoco
es correcto imprimir los programas utilizando un tipo de letra proporcionado 1 como “Times” o
“Helvética” sino que los programas deben imprimirse en tipos de letra monoespaciados como
por ejemplo “Courier”.
Por último destacar las siguientes nociones:

la sentencia inicial siempre se ejecuta

la condición siempre se comprueba antes de cada iteración. Por lo tanto puede ocurrir
que nunca se llegue a entrar en el bucle si desde el principio no se cumple la condición.
Por ejemplo for(i=0; i<-3; i++) nunca entra en el bucle.

la sentencia final se ejecuta al terminar cada iteración. Por lo tanto si no se entra en el


bucle esta sentencia no se ejecuta nunca.

5.2.1. Operadores relaciones


Veamos ahora una breve introducción a las operadores relacionales que pueden utilizarse
para escribir la condición del bucle.
Los operadores relacionales se utilizan para poder comparar los valores de distintas vari-
ables o para comparar el valor de una variable con una constante. En principio todas las com-
paraciones deben realizarse entre variables y constantes del mismo tipo, aunque se aplican las
conversiones automáticas de tipos descritas en el capı́tulo anterior.
Los operadores relaciones son:
mayor que: >

menor que: <

mayor o igual a: >=

menor o igual a: <=

igual a: ==

distinto de: !=
1
Los tipos de letra proporcionados son aquellos en los que cada letra tiene una anchura diferente, por ejemplo la
letra ‘m’ es más ancha que la letra ‘l’, mientras los tipos monoespaciados son aquellos en los que todas las letras y
signos tienen la misma anchura.
46 CAPÍTULO 5. CONTROL DE FLUJO: FOR

No debe confundirse el operador de asignación = con el operador relacional de igualdad


== que no modifica el valor de las variables. Es decir, es totalmente distinto escribir i=7, que
introduce el valor 7 en la variable i independientemente del valor que tuviese ésta, que escribir
i==7, que compara el valor de la variable i con la constante 7.

Ejercicio
1. Comprobar que otra forma de escribir el bucle del ejemplo serı́a poner for(i=0; i!=3;
i++).

5.3. Doble bucle for


También se pueden hacer bucles dentro de otros bucles, sólo hace falta tener cuidado al
colocar las llaves y tener cuidado de utilizar variables de control diferentes. Pensemos por
ejemplo en un robot que tiene que cortar un césped de 20 m de ancho por 5 m de largo. Un
programa para hacer esto serı́a:
/*********************
Programa: CortaCesped.c
Descripción: Programa para manejar un robot que corta el cesped.
Revisión 0.0: 12/MAR/1998
Autor: Rafael Palacios
*********************/

#include <stdio.h>
#include <robot.h> /*Declara las funciones para el manejo del robot*/

main(void)
{
int i,j;

/*Inicialmente el robot está en la esquina


R********************
*********************
*********************
*********************
*********************

El robot debe ir avanzando hacia la derecha mientras corta el césped


y al llegar al final retrocede y baja un metro.

....................
...R****************
********************
********************
********************
*/

for(i=0; i<5; i++) {


ColocarEnPosicion(); /*Se coloca mirando a la derecha*/
5.3. DOBLE BUCLE FOR 47

for(j=0; j<20; j++) {


AvanzaDerecha();
printf("He avanzado un metro\n");
}
printf("He llegado al final, vuelvo\n");
RegresarIzquierda();
BajarUnMetro();
}
printf("Trabajo terminado. Esto es fácil.\n");
}
En caso de bucles anidados es más importante todavı́a tener cuidado con utilizar un san-
grado correcto, de manera que se identifique claramente el comienzo y el final de cada bu-
cle. En este ejemplo también se puede observar que no es lo mismo escribir la instrucción
ColocarEnPosicion() antes del for interno que después de la llamada a BajarUnMetro().
Aunque parezca lo mismo, sólo se garantiza que el robot empieza a cortar el césped en la
dirección adecuada cuando el programa está escrito como en el ejemplo. Es importante tener
cuidado con las inicializaciones.

Ejercicios
1. Escribir un programa que escriba con printf todos los números del 0 al 20.
2. Modificar el programa anterior para que escriba sólo los números pares del 2 al 20.
48 CAPÍTULO 5. CONTROL DE FLUJO: FOR
Capı́tulo 6

Control de flujo: while,


do-while

6.1. Introducción
Imaginemos que tenemos un robot en una habitación como la mostrada en la figura 6.1.
El robot sólo obedece a dos instrucciones sencillas: avanzar un centı́metro hacia adelante y
girar g grados a la derecha. Nuestra tarea es conseguir que el robot salga por la puerta, pero no
sabemos a priori donde está, por lo que una solución a base de un bucle for como el estudiado
en el tema anterior que avance el robot hasta la pared, una instrucción de giro de 90 grados,
otro bucle para que avance hasta la puerta y un último giro de -90 grados para salir no nos
sirve, al no conocer a priori el número de iteraciones que deseamos realizar en cada bucle.

bip bip

Figura 6.1: Robot

Como futuros ingenieros que somos no nos vamos a rendir fácilmente ante semejante
problema. Una solución es colocar al robot un sensor que nos diga cuando toca la pared, y
hacer avanzar el robot hasta que toque la pared, girar 90 grados, y seguir avanzando pegado a
la pared, lo que seguirá siendo detectado por nuestro estupendo sensor, hasta que lleguemos a
la puerta, momento en el cual giraremos -90 grados para salir, cumpliendo con éxito nuestro
objetivo. El algoritmo descrito se puede expresar usando pseudoc ódigo1 como:
1 Un pseudocódigo es una manera de expresar un algoritmo usando lenguaje natural y a un nivel de detalle muy
alto.

49
50 CAPÍTULO 6. CONTROL DE FLUJO: WHILE, DO-WHILE

mientras que el robot no toque la pared {


avanza_1_cm_adelante(); /* Avanza hacia la pared */
}
girar(90);
mientras que el robot toque la pared {
avanza_1_cm_adelante(); /*Avanza pegado a la pared hacia la puerta*/
}
girar(-90); /* Se pone delante de la puerta */
avanza_1_cm_adelante(); /* Sale por la puerta! */
Como podemos apreciar, el algoritmo consiste en repetir una serie de instrucciones mien-
tras una condición es cierta. Para ello todos los lenguajes estructurados poseen unas sentencias
de control especı́ficas, que el caso del C son los bucles while y do-while.

6.2. Sintaxis del bucle while


Este tipo de bucles repiten una serie de instrucciones mientras una condición es cierta. La
sintaxis es:

while(condición){
instrucción 1;
instrucción 2;
...
instrucción n;
}

El funcionamiento del bucle es como sigue: en primer lugar se evalúa la expresión condi-
ción. Si el resultado es falso no se ejecutará ninguna de las instrucciones del bucle, el cual
está delimitado, al igual que en el caso del bucle for por dos llaves ({ y }). Por tanto la ejecu-
ción continuará después de la llave }. Si por el contrario la condición es cierta, se ejecutarán
todas las instrucciones del bucle. Después de ejecutar la última instrucción del bucle (instruc-
ción n;) se vuelve a comprobar la condición y al igual que al principio se terminará el bucle
si es falsa o se realizará otra iteración si es cierta, y ası́ sucesivamente.

6.2.1. Ejemplos
Veamos a continuación algunos ejemplos del uso del bucle while.

Sacar al robot de la habitación


Se desea resolver en C el problema propuesto en la introducción. El fabricante del robot,
muy gentilmente, nos ha cedido varias rutinas en C para que su manejo sea más fácil. Estas
rutinas son:

avanza 1 cm adelante() que hace avanzar al robot un centı́metro hacia adelante.


gira(double angulo) que es una función que admite un double como parámetro y
que hace que el robot gire a derechas el número de grados indicado en dicho parámetro.
toca pared() que es una función que vale uno si el robot toca la pared y cero si no la
toca.
6.2. SINTAXIS DEL BUCLE WHILE 51

El ejemplo descrito mediante pseudocódigo en la introducción se realizarı́a en lenguaje C


de la siguiente manera:

/* Programa: SacaRob
*
* Descripción: Saca al robot de una habitación como la mostrada en
* la figura 6.1.
*
* Revisión 0.0: 10/03/1998
*
* Autor: El robotijero loco.
*/

#include <stdio.h>
#include <robot.h> /* Declara las funciones del robot */

main(void)
{
while(toca_pared() != 1){
avanza_1_cm_adelante(); /* Avanza hacia la pared */
}

girar(90);

while(toca_pared() == 1){
avanza_1_cm_adelante(); /*Avanza pegado a la pared hacia la puerta*/
}

girar(-90); /* Se pone delante de la puerta */

avanza_1_cm_adelante(); /* Sale por la puerta! */


}

Suma de series.
A nuestro hermanito pequeño le han mandado en la escuela la ardua tarea realizar las
sumas siguientes:

1  27  34  54

2  34  356  1234  34  2378  3

23  2345

Como nuestro hermanito pequeño no tiene aún calculadora por haberse gastado el presupuesto
familiar previsto para el caso en chicles, nos pide que le dejemos la nuestra. El problema es
que se acaban de gastar las pilas y es domingo por la tarde, con lo que sólo tenemos dos
opciones: hacer las sumas a mano o realizar un pequeño programa en C, lo cual parece ser
lo más razonable, dado que existen fundadas expectativas de que el próximo dı́a de clase el
malvado profesor de matemáticas vuelva a atormentar a nuestro hermanito con más sumas
enormes.
52 CAPÍTULO 6. CONTROL DE FLUJO: WHILE, DO-WHILE

Como buenos programadores que somos ya a estas alturas, antes de encender el ordenador
tomamos papel y lápiz para analizar el problema, el cual no es más que sumar una serie de
números. Lo primero que se nos ocurre es realizar un programa que realice las sumas del
problema de nuestro hermanito: solución fácil, pero poco apropiada, pues sólo es válida para
este ejercicio y no para los que se avecinan. Lo siguiente que se nos ocurre es realizar un bucle,
que vaya pidiendo los números por el teclado y los vaya sumando. El problema ahora es que el
número de sumandos varı́a de una suma a otra. La solución a este problema es doble: podemos
pedir al usuario al principio del programa el número de sumandos y con esta información usar
un bucle for; pero el inconveniente de esta solución es que hay que contar antes todos los
números, lo cual, aparte de ser un rollo, es propenso a errores. La segunda solución es usar
un bucle while y decirle al usuario que después del último número introduzca un cero para
indicar al programa el final de la serie. Un posible pseudocódigo serı́a:

pedir número;
mientras número no sea cero {
sumar el número a la suma anterior;
pedir número;
}
imprimir total;

De este pseudocódigo lo único que nos queda por resolver es cómo hacer para sumar
el número a la suma anterior. Este tipo de operaciones son muy comunes en C y se
resuelven creando una variable, inicializándola a cero antes de entrar en el bucle y dentro
del bucle se incrementa con el valor deseado, es decir:

int suma;

suma = 0; /* Inicialización */

bucle{ /* bucle for, while... */


suma += lo_que_sea;
}
hacer lo que sea con suma;

En este pseudocódigo primero se define la variable suma como entera (obviamente el tipo
en cada caso será el que haga falta). Después hay que inicializarla, pues dentro del bucle se
hace suma += lo que sea que como recordará es una manera abreviada de expresar suma
= suma + lo que sea y por tanto si la variable suma no se inicializa antes de entrar en el
bucle, el valor de dicha variable puede ser cualquier cosa, con lo que el valor de la suma
final será erróneo y el suspenso de nuestro hermanito cargará para siempre sobre nuestras
conciencias.
Una vez resueltos todos los problemas de nuestro programa podemos pasar a escribirlo en
C, quedando como:

01 /* Programa: SumaSer
02 *
03 * Descripción: Suma una serie de números introducidos por teclado. La
04 * serie se termina introduciendo un cero, momento en el
05 * que se imprime el total de la suma por pantalla.
06 *
07 * Revisión 0.0: 10/03/1998
6.3. SINTAXIS DEL BUCLE DO-WHILE. 53

08 *
09 * Autor: El hermano del hermanito sin calculadora.
10 */
11
12 #include <stdio.h>
13
14 main(void)
15 {
16 int suma; /* Valor de la suma */
17 int numero; /* numero introducido por teclado */
18
19 printf("Introduzca los números a sumar. Para terminar teclee un 0\n");
20
21 suma = 0; /* inicializo el total de la suma */
22
23 scanf("%d",&numero); /* pido el primer número */
24 while(numero != 0){
25 suma += numero;
26 scanf("%d",&numero);
27 }
28
29 printf("El valor de la suma es: %d\n", suma);
30 }

6.3. Sintaxis del bucle do-while.


En la sección anterior se ha visto que el bucle while comprueba su condición antes de
ejecutar el bucle. Esto nos obliga a que si la condición depende de alguna instrucción eje-
cutada dentro del bucle, esa misma instrucción hay que realizarla antes del bucle, tal como
ocurrió en la lı́nea 23 del ejemplo anterior. Para simplificar este tipo de situaciones existe el
bucle do-while, el cual hace una primera pasada por el bucle, comprobando su condición al
final, terminando el bucle si ésta es falsa, o volviéndolo a ejecutar si es cierta. La sintaxis es:

do{
instrucción 1;
instrucción 2;
...
instrucción n;
}while(condición);

En este caso se ejecutarán las instrucciones instrucción 1. . . instrucción n y después se


evaluará la condición. Si es falsa se continúa con la instrucción que sigue al bucle y si es
cierta se vuelve a repetir el bucle (instrucción 1. . . instrucción n), se evalúa la condición y
ası́ sucesivamente.

6.3.1. Ejemplos
Como el movimiento se demuestra andando, vamos a ver un ejemplo del bucle do-while.
54 CAPÍTULO 6. CONTROL DE FLUJO: WHILE, DO-WHILE

Suma de series. Versión 1.0


Vamos a resolver el problema de la suma de series de la sección 6.2.1 usando un bucle
do-while. En este caso, después de la fase de análisis que se realizó en dicha sección llegamos
a que otro posible pseudocódigo que soluciona el mismo problema es:

hacer{
pedir número;
sumar el número a la suma anterior;
}mientras número no sea cero;
imprimir total;

Que expresado en C resulta:

01 /* Programa: SumaSer
02 *
03 * Descripción: Suma una serie de números introducidos por teclado. La
04 * serie se termina introduciendo un cero, momento en el
05 * cual se imprime el total de la suma por pantalla
06 *
07 * Revisión 1.0: 10/03/1998
08 *
09 * Autor: El hermano del hermanito sin calculadora.
10 */
11
12 #include <stdio.h>
13
14 main(void)
15 {
16 int suma; /* Valor de la suma */
17 int numero; /* numero introducido por teclado */
18
19 printf("Introduzca los números a sumar. Para terminar teclee un 0\n");
20
21 suma = 0; /* inicializo el total de la suma */
22
23 do{
24 scanf("%d",&numero);
25 suma += numero;
26 }while(numero != 0);
27
28 printf("El valor de la suma es: %d\n", suma);
29 }

Comparando esta solución con la anterior usando un bucle while, vemos que la única
diferencia es que ahora no hace falta pedir el primer número antes de comenzar el bucle, pues
éste se ejecuta al menos una vez, comprobándose la condición al final. Por tanto, es posible
leer el número dentro del bucle y comprobar si es cero al final de éste.
Otra cosa que cambia es el orden de las dos instrucciones que hay dentro del bucle. Se deja
como ejercicio la explicación del porqué de semejante cambio.
6.4. ¿ES EL BUCLE FOR IGUAL QUE EL WHILE? 55

6.4. ¿Es el bucle for igual que el while?


Algún alumno aventajado se habrá dado cuenta de que el bucle while y el for son muy
parecidos. En realidad cualquier cosa que se realice con uno de ellos se puede realizar con el
otro. Ası́ un bucle while genérico como:

while(condición){
instrucción 1;
instrucción 2;
...
instrucción n;
}

Se realizarı́a con un for de la siguiente manera:

for( ; condición ; ){
instrucción 1;
instrucción 2;
...
instrucción n;
}

En donde se aprecia que no se ha puesto nada en la condición inicial que como recordarán
iba entre el primer paréntesis del for y el primer ; ni en la condición final que iba después
del último ; y antes del paréntesis que cierra el for. El compilador en este caso no da ningún
error, sino que simplemente no hace nada antes de empezar el bucle ni tampoco hace nada
al final de cada iteración. Por tanto el funcionamiento de este bucle for es el siguiente: al
principio evalúa la condición, si es falsa se continúa después del bucle y si es cierta se ejecuta
el bucle, volviéndose a comprobar la condición y repitiéndose el ciclo. Dicho comportamiento
como podrá apreciar mi querido lector es el explicado no hace muchas lı́neas atrás cuando se
intentaba exponer el funcionamiento del bucle while.
Del mismo modo un bucle for se puede expresar mediante un while, aunque no tan
elegantemente. Ası́ el bucle for:

for(sentencia inicial ; condición ; sentencia final ){


instrucción 1;
instrucción 2;
...
instrucción n;
}

mediante un bucle while se expresarı́a como:

sentencia inicial;
while(condición){
instrucción 1;
instrucción 2;
...
instrucción n;
sentencia final;
56 CAPÍTULO 6. CONTROL DE FLUJO: WHILE, DO-WHILE

6.4.1. Entonces ¿cuál elijo?


La elección entre un tipo de bucle u otro debe hacerse intentando conseguir la mayor
claridad posible del código escrito. Si se desea repetir un conjunto de instrucciones mientras
una condición sea cierta, entonces lo más natural es usar un bucle while, pues muestra mucho
más claramente lo que se desea hacer. Si en cambio queremos realizar una determinada tarea
un determinado número de veces, lo más claro es usar un bucle for, que incluye dentro de la
sentencia de control las instrucciones que permiten inicializar la variable que hace de contador
e incrementarla al final de cada iteración.

6.5. El ı́ndice del bucle for sirve para algo


En los ejemplos dados hasta ahora, la variable que permitı́a llevar la cuenta de las veces
que ejecutábamos el bucle for se usaba única y exclusivamente para eso. Sin embargo, al ser
una variable normal y corriente, es posible usar dicha variable dentro del bucle, pero eso sı́,
no es muy recomendable cambiar su valor, pues se generan programas confusos y por tanto
difı́ciles de mantener. Veamos un ejemplo para aclarar este tema: supongamos que acabamos
de llegar a casa después de una clase de matemáticas en la que nos han explicado las series
aritméticas y nos han dado la fórmula:
 
1  2  3 
 

2
1
n
nn 

El problema es que como estábamos hablando con el compañero (como de costumbre),


justo cuando terminábamos de copiar la fórmula el profesor la borró de la pizarra, con lo que
no estamos muy seguros de ella. Como ya estamos hechos unos artistas del C, decidimos hacer
un programilla que calcule el valor de una serie de n números y comprobar ası́ la fidelidad de
nuestros apuntes. El pseudocódigo del programa es muy simple:

pedir valor final n;


inicializar suma total;
for(i=1; i<=n; i++){
sumar i a la suma total;
}
imprimir suma total;
calcular el valor de la serie e imprimirla;

Una vez comprobado que el pseudocódigo realiza lo que queremos pasamos a la acción,
codificando el siguiente programa:

1 /* Programa: SerAri
2 *
3 * Descripción: Suma una serie aritmética de 1 a n, siendo n un
4 * dato introducido por el usuario, imprimiendo el
5 * valor de la suma y el de la formula n*(n+1)/2.
6 *
7 * Revisión 0.0: 10/03/1998
8 *
6.6. EJERCICIOS 57

9 * Autor: El alumno charlatán.


10 */
11
12 #include <stdio.h>
13
14 main(void)
15 {
16 int suma; /* Valor de la suma */
17 int numero; /* Valor final de la serie */
18 int i; /* ı́ndice del for */
19
20 printf("Introduzca el número final de la serie.\n");
21 scanf("%d", &numero);
22
23 suma = 0; /* inicializo el total de la suma */
24
25 for(i=1; i<=numero; i++){
26 suma += i;
27 }
28
29 printf("El valor de la suma es: %d\n", suma);
30 printf("Y el de la fórmula es: %d\n", numero*(numero+1)/2 );
31 }
En este programa se han introducido dos novedades: la primera ha sido la anunciada pre-
viamente de usar la variable de control del bucle i dentro del bucle (lı́nea 26). La segunda es
la de usar una expresión dentro del printf en la linea 30. El funcionamiento de esta instruc-
ción es el siguiente: se evalúa en primer lugar la expresión numero*(numero+1)/2 y el valor
resultante es el que se imprime en pantalla.

6.6. Ejercicios
1. Realizar un programa para calcular multiplicaciones del tipo:
2 * 3 * 35
345 * 34 * 3.5 * 23 * 2.1
2 * 2.234
Hacer dos versiones: una en la que se pida al usuario al principio del programa la canti-
dad de números que desea multiplicar para pasar seguidamente a pedirle cada uno de los
números a multiplicar. En la otra versión el usuario introducirá uno a uno los números,
indicándole al programa que ya no hay más mediante la introducción de un 1.
2. Realizar un programa para comprobar la veracidad de la siguiente fórmula para calcular
la suma de una serie geométrica:

1  3  5    2



n 1  n2
Pistas: La instrucción final de los bucles for puede ser también i+=2. Para calcular n2 se puede
hacer n*n
58 CAPÍTULO 6. CONTROL DE FLUJO: WHILE, DO-WHILE
Capı́tulo 7

Control de flujo: if

7.1. Introducción
Es habitual que los programas realicen tareas diferentes en función del valor de determi-
nadas variables. La instrucción if permite definir bloques de código que no son de ejecución
obligatoria y por lo tanto son bloques de instrucciones que el programa se puede saltar. Esta
decisión depende del valor de una condición lógica, que a su vez suele depender del valor que
adquieran determinadas variables del programa. También es posible definir dos bloques de
instrucciones alternativos, de manera que sólo se ejecute uno u otro en función del resultado
de la condición lógica.

7.2. Sintaxis del bloque if


Existen dos formatos básicos para definir instrucciones que se ejecutan de manera condi-
cional. Un bloque que se puede ejecutar o no (if normal), y dos bloques que se ejecutan uno
u otro (bloque if-else). El formato en cada caso es el siguiente:

if normal:
if (condición) {
instrucción 1;
instrucción 2;
...
instrucción n;
}

bloque if-else:
if (condición) {
instrucción 1.1;
instrucción 1.2;
...
instrucción 1.n;
} else {
instrucción 2.1;
instrucción 2.2;
...

59
60 CAPÍTULO 7. CONTROL DE FLUJO: IF

instrucción 2.m;
}

Veamos como ejemplo un programa que pregunta el valor de dos números enteros y que
escribe en pantalla cuál es el mayor de los dos. Este programa debe utilizar scanf para pre-
guntar los valores y luego un if para decidir si se escribe el primero o el segundo.

/*********************
Programa: Compara.c
Descripción: Lee dos números del teclado y los compara
Revisión 0.0: 16/FEB/1999
Autor: Rafael Palacios
*********************/

#include <stdio.h>

main(void)
{
int a,b;

printf("Dame un valor entero ");


scanf("%d",&a);
printf("Dame otro valor ");
scanf("%d",&b);

if (a>b){
printf("El mayor es %d\n",a);
} else {
printf("El mayor es %d\n",b);
}
}

Otra aplicación muy interesante es evitar errores de cálculo en los programas, como por
ejemplo las divisiones por cero o las raı́ces cuadradas de números negativos. El siguiente
ejemplo es un programa que incluye un control para evitar las divisiones por cero. En realidad
el ejemplo parece algo tonto porque en caso de error simplemente termina con un exit, pero
un programa más completo puede tomar otras medidas como por ejemplo volver a preguntar
el valor del denominador.
La función exit() hace que el programa termine inmediatamente y le devuelve al sistema
operativo el valor de su argumento1. Esta manera de terminar un programa antes de que llegue
la ejecución al final de la función main, sólo está permitida en caso de detectarse un error que
impida continuar con la ejecución normal del programa.

/*********************
Programa: Division.c
Descripción: Calcula el cociente de dos números.
El programa incluye un control que evita dividir por cero.
Revisión 0.0: 16/FEB/1999
1 Por convenio, un programa que termina a causa de un error debe devolver al sistema operativo un valor distinto

de cero.
7.3. FORMATO DE LAS CONDICIONES 61

Autor: Rafael Palacios


*********************/

#include <stdio.h>
#include <stdlib.h>

main(void)
{
int numerador, denominador;
double division;

printf("Numerador? ");
scanf("%d",&numerador);
printf("Denominador? ");
scanf("%d",&denominador);

if (denominador==0) {
printf("Error, el resultado es infinito\n");
exit(1);
}
division=(double)numerador/denominador;
printf("La división vale: %f\n",division);
}

7.3. Formato de las condiciones


Las condiciones que aparecen en los bloques if son las mismas que pueden aparecer en los
bucles while, do-while y en el segundo término de los bucles for. Son expresiones que nor-
malmente incluyen operadores lógicos y relacionales. Este apartado resume estos operadores,
aunque muchos de ellos ya se han visto en capı́tulos anteriores.

7.3.1. Operadores relacionales


Son operadores que permite comparar variables y/o constantes entre si.
Los operadores relaciones son:
mayor que: >
menor que: <
mayor o igual a: >=
menor o igual a: <=
igual a: ==
distinto de: !=
Las reglas de precedencia establecen que los operadores >, >=, <, <= se evalúan en
primer lugar y posteriormente se evalúan los operadores == y !=. Esta regla no es muy im-
portante ya que normalmente no se mezclan estos operadores en la misma condición. Más
interesante es saber que la precedencia de los operadores matemáticos es mayor que la de los
62 CAPÍTULO 7. CONTROL DE FLUJO: IF

operadores lógicos y por lo tanto condiciones como la siguiente se evalúan realizando primero
los cálculos y luego las comparaciones:

(a > 3*b) /* Esto hace ( a > (3*b) ) */


(a+b == 0) /* Esto hace ( (a+b) == 0 ) */

7.3.2. Operadores lógicos


Son operadores que permiten relacionar varias expresiones. Las operaciones básicas son
AND (la condición es cierta sólo si las dos expresiones son ciertas), OR (la condición es
cierta si una o las dos expresiones son ciertas) y la negación (invierte el resultado lógico de la
expresión que le sigue).
Los operadores lógicos son:
AND lógico: &&
OR lógico: ||
negación: !
Estos operadores permiten definir condiciones complicadas en todos los bucles y en los
if. Hay que tener en cuenta que la precedencia del operador && es mayor que la del operador
||.

(a>3 && b==4 || c!=7) /* Esto hace ( (a>3 && b==4) || c!=7 ) */

En caso de duda lo mejor es poner los paréntesis necesarios antes que perder el tiempo
haciendo pruebas en el programa. Es decir, o se mira la documentación y se hacen las cosas
bien a la primera o se ponen paréntesis hasta que la operación quede clara. Esto último tiene
la ventaja de que estará claro para todo el que lea el programa, aunque no se acuerde muy bien
de las precedencias de los operadores.
Por último conviene destacar que en C los operadores relacionales son operadores binarios,
es decir, realizan la comparación entre los operandos que están a su derecha y a su izquierda;
siendo el resultado de dicha comparación un 1 si la condición es cierta o un 0 si es falsa. Si,
acostumbrados a la notación matemática, se escribe:
0 <= a <= 10
para expresar la condición 0 a 10, el resultado de dicha expresión será siempre 1, inde-
pendientemente del valor de la variable a. ¿Puede adivinar por qué? Si se tiene en cuenta que
en C las expresiones se evalúan de izquierda a derecha, en primer lugar se evaluará el primer
operador <=, el cual compara el operando que está a su izquierda (0) con el que está a su
derecha (a). El resultado de esta expresión será 0 si a es menor que 0 o 1 en caso contrario.
Dicho resultado será el operando izquierdo del siguiente operador relacional, con lo que se
evaluará la expresión 0 <= 10 o 1 <= 10 según haya sido el resultado de la comparación
anterior. En cualquier caso el resultado final de la expresión será cierto (1).
Para expresar en C la condición 0 a 10 correctamente, es necesario dividir la relación
en dos, es decir, en primer lugar comprobar si a es mayor o igual que 0 y en segundo lugar si a
es menor o igual que 10. Por último es necesario usar un operador lógico para relacionar ambas
expresiones, de forma que el resultado sea cierto cuando ambas condiciones sean ciertas. ¿Se
atreve a escribir la condición? Por si acaso no se ha atrevido, ésta se muestra a continuación:
(0 <= a) && (a <= 10)
7.4. VALORES DE VERDADERO Y FALSO 63

7.4. Valores de verdadero y falso


Todas las operaciones lógicas y relacionales devuelven un valor tipo int que puede ser 1
para verdadero o 0 para falso. Por lo tanto las condiciones que se escriben en los bucles o en
los if suelen tener valor 1 ó 0. Sin embargo las condiciones se consideran falsas sólo cuando
valen 0 y verdaderas cuando valen cualquier otra cosa. En este sentido cualquier variable de
tipo entero puede utilizarse como condición y hará que la condición sea falsa sólo si su valor
es cero. Esta es la razón fundamental para que en C no sea necesario un tipo de variable
para operaciones lógicas (que sólo pueda valer 1 ó 0) como ocurre en otros lenguajes de
programación. En C la siguiente lı́nea es válida:

i=a>3;

En esta instrucción la variable i toma el valor 1 ó 0 en función del valor que tenga la
variable a. Además las siguientes condiciones son equivalentes para comprobar si una variable
entera vale verdadero:

if (a!=0) {
printf("Verdadero!\n");
}
if (a) {
printf("Verdadero!\n");
}

lo mismo ocurre para comprobar si una variable es cero o falso, se puede preguntar de dos
maneras:

if (a==0) {
printf("Falso\n");
}
if (!a) {
printf("Falso\n");
}

En definitiva, la única definición categórica es que 0 es falso.


En ambos casos queda más clara la primera expresión lógica (if(a!=0)) que la segunda
(if(a)).

7.5. Bloque if else-if


Es muy habitual en los programas tener que realizar varias preguntas en cadena, de man-
era que la estructura del bloque if-else se puede complicar bastante. Veamos el siguiente
ejemplo:

/*********************
Programa: Alturas1.c
Descripción: Pregunta la altura de una persona
Revisión 0.0: 16/FEB/1999
Autor: Rafael Palacios
*********************/
64 CAPÍTULO 7. CONTROL DE FLUJO: IF

#include <stdio.h>

main(void)
{
double altura;

printf("Cuál es su altura? ");


scanf("%lf",&altura);

if (altura<1.5) {
printf("Usted es bajito\n");
} else {
if (altura<1.7) {
printf("Usted no es alto\n");
} else {
if (altura<1.9) {
printf("Usted es alto\n");
} else {
printf("Juega al baloncesto?\n");
}
}
}
printf("Fin del programa.\n");
}

En este tipo de estructuras de programa hay que ser muy cuidadoso para colocar correcta-
mente las llaves y es imprescindible hacer un buen uso del sangrado para facilitar la lectura del
programa. Únicamente remarcar que el siguiente programa aunque es mucho más compacto
no hace lo mismo que el anterior:

/*********************
Programa: Alturas2.c
Descripción: Pregunta la altura de una persona
Revisión 1.0: 16/FEB/1999
Autor: Rafael Palacios
*********************/

#include <stdio.h>

main(void)
{
double altura;

printf("Cuál es su altura? ");


scanf("%lf",&altura);

if (altura<1.5){
printf("Usted es bajito\n");
}
if (altura<1.7){
7.5. BLOQUE IF ELSE-IF 65

printf("Usted no es alto\n");
}
if (altura<1.9){
printf("Usted es alto\n");
}else{
printf("Juega al baloncesto?\n");
}
printf("Fin del programa.\n");
}
Este programa es incorrecto porque por ejemplo para alturas de 1.4 m se escriben tres
mensajes en lugar de uno, ya que este valor cumple las tres condiciones de los tres if. En el
caso del primer programa, al ser cierto el primer if, se escribe Es usted bajito y se salta
directamente al último printf del programa porque el resto de las instrucciones forman parte
del else del primer if.
Para este tipo de situaciones se puede usar el bloque if else-if, cuya sintaxis es:

if (condición) {
instrucción 1.1;
instrucción 1.2;
...
instrucción 1.m;
} else if (condición) {
instrucción 2.1;
instrucción 2.2;
...
instrucción 2.n;
...
} else {
instrucción 3.1;
instrucción 3.2;
...
instrucción 3.p;
}

El funcionamiento de esta construcción es el siguiente: Se evalúa la condici ón del primer


if. Si es cierta se ejecuta el bloque perteneciente a este if. Si es falsa se evalúa la condici ón
del else if que le sigue, ejecutándose su bloque si dicha condici ón es cierta o pasando al
siguiente else if si es falsa. Si ninguna de las condiciones es cierta se ejecutará el bloque
del último else en caso de que exista, pues es opcional su uso.
Usando el bloque if else-if el programa anterior se puede escribir, de una manera mu-
cho más clara como:
/*********************
Programa: Alturas3.c
Descripción: Pregunta la altura de una persona
Revisión 0.0: 16/FEB/1999
Autor: Rafael Palacios
*********************/

#include <stdio.h>
66 CAPÍTULO 7. CONTROL DE FLUJO: IF

main(void)
{
double altura;

printf("Cuál es su altura? ");


scanf("%lf",&altura);

if (altura<1.5){
printf("Usted es bajito\n");
}else if (altura<1.7){
printf("Usted no es alto\n");
}else if (altura<1.9){
printf("Usted es alto\n");
}else{
printf("Juega al baloncesto?\n");
}

printf("Fin del programa.\n");


}

7.6. Ejercicios
1. Repetir el programa anterior pero sin utilizar else. Es decir, preguntando en el mismo
if si altura es menor que 1.7 y mayor o igual a 1.5. Pista: Hay que utilizar los operadores
lógicos.
Capı́tulo 8

Control de flujo: switch-case,


break y continue

8.1. Introducción
En este tema vamos a estudiar las últimas sentencias de control de flujo de C. En primer
lugar vamos a estudiar la construcción switch-case, que es usada para ejecutar un trozo de
código u otro en función del valor de una variable de control. Para finalizar se estudiarán las
sentencias break y continue que permiten modificar el flujo normal de ejecución dentro de
un bucle.

8.2. La construcción switch-case


8.2.1. Introducción
En muchos programas es necesario realizar una tarea u otra en función de una variable,
ya sea ésta el resultado de un cálculo o un valor introducido por el usuario. Imaginemos que
tenemos un robot como el descrito en el capı́tulo 6 y que deseamos realizar un controlador
interactivo, es decir, un programa mediante el cual el usuario introduzca una serie de coman-
dos al programa y éste mueva al robot según dichos comandos. Como se recordará, el robot
sólo obedecı́a a dos instrucciones básicas: “avanzar un centı́metro hacia adelante” y “girar un
determinado número de grados a la derecha”. Además disponı́a de un avanzado sensor que
detectaba si el robot estaba tocando algún obstáculo. Como también recordará, disponı́amos
de tres funciones que nos habı́a suministrado muy gentilmente el fabricante del robot para
gobernarlo (al robot, no al fabricante, claro está). Estas rutinas también se describieron en el
capı́tulo 6, pero las volvemos a poner a continuación para mayor comodidad del lector:

avanza 1 cm adelante() hace avanzar al robot un centı́metro hacia adelante.


gira(double angulo) es una función que admite un double como parámetro y que
hace que el robot gire a derechas el número de grados indicado en dicho parámetro.
toca pared() es una función que vale uno si el robot toca la pared y cero si no la toca.

Para que el robot pueda ser gobernado de forma interactiva, el programa deberı́a pedir
al usuario un comando y actuar en consecuencia. Dicho comando podrı́a ser cualquier cosa,

67
68 CAPÍTULO 8. CONTROL DE FLUJO: SWITCH-CASE, BREAK Y CONTINUE

aunque lo más cómodo es un número o una letra: por ejemplo 1 significarı́a avanzar hacia
adelante y 2 girar un número de grados (que habrá que preguntarle previamente al usuario).
Un posible pseudocódigo del programa serı́a:

pedir comando al usuario;


si es 1:
avanza_1_cm_adelante();
si es 2:
pedir número de grados;
gira(número de grados);

Este tipo de situaciones son muy comunes en todos los programas. Por ello, aunque este
tipo de decisiones se podrı́an resolver con sentencias if de la forma:

if(comando == 1){
avanza_1_cm_adelante();
}else if (comando == 2){
pedir número de grados;
gira(número de grados);
}

algunos lenguajes poseen estructuras de control especı́ficas para resolver más fácilmente este
tipo de decisiones. En el caso de C esta estructura de control es la construcción switch-case.

8.2.2. Sintaxis de la construcción switch-case


La sintaxis de esta construcción es:

switch(expresión){
case constante 1:
instrucción 1 1;
...
instrucción 1 n1;
break;
...
case constante n:
instrucción n 1;
...
instrucción n nn;
break;
default:
instrucción d 1;
...
instrucción d nd;
break;
}

Antes de comenzar a explicar los entresijos del funcionamiento de esta estructura de con-
trol conviene destacar algunos aspectos de su sintaxis:

Después de la palabra clave case y de su constante el sı́mbolo que sigue son dos puntos
(:) y no un punto y coma (;).
8.2. LA CONSTRUCCIÓN SWITCH-CASE 69

Al final de cada bloque de instrucciones de un case se escribe la sentencia break.


Sólo hay dos llaves: la de después del switch y la del final. Los bloques de instrucciones
pertenecientes a cada uno de los case vienen delimitados por el case y por el break
del final.
Una vez advertido esto, pasemos a describir el funcionamiento de esta construcción. En
primer lugar se evalúa la expresión que sigue al switch, con lo que se obtendrá un valor que ha
de ser entero. Este valor se compara entonces con la constante entera que sigue al primer case
(la constante 1). Si el resultado de la comparación es falso se prueba suerte con el siguiente
case y ası́ sucesivamente hasta encontrar algún case cuya constante sea igual al resultado de
la expresión. Si esto ocurre se ejecutarán las instrucciones situadas entre el afortunado case
y el break que finaliza su bloque de instrucciones, continuando después con las instrucciones
que sigan a la llave que cierra el switch. Si ningún case tiene a su lado una constante que
coincida con el valor de la expresión, entonces se ejecutarán las instrucciones situadas entre
el default y el último break, y luego se continuará con las instrucciones que siguen después
del switch.
Conviene hacer notar que se puede escribir un bloque switch-case sin el apartado de
default. Si esto ocurre y se da el desafortunado caso de que ninguno de los case contenga
el valor de la constante resultante de la evaluación de la expresi ón, entonces la ejecución del
programa continúa tranquilamente después de la llave que cierra el switch-case.
Para aclarar ideas, veamos el siguiente ejemplo de un switch-case:
switch (a){
case 1:
printf("a vale uno\n");
break;
case 2:
printf("a vale dos\n");
break;
default:
printf("a no vale ni uno ni dos\n");
break;
}
Supongamos que a vale 1. Como acabamos de decir, en primer lugar se evalúa la expresión
que hay entre los paréntesis situados después del switch. En este caso la expresión es simple-
mente una variable, por lo que el resultado de la expresión será el valor de la variable, es decir
un 1. El programa compara entonces el valor ası́ obtenido con el que hay a continuación del
primer case, que como hemos tenido mucha suerte, es también un 1, con lo que se ejecutará el
conjunto de instrucciones situadas entre este case y el siguiente break que en este ejemplo
tan simple consta tan solo de la instrucción printf("a vale uno\n").

8.2.3. Ejemplos
Veamos a continuación algunos ejemplos para ilustrar el uso de la construcción switch-case.

El robot con control manual


Continuando con el ejemplo presentado en la introducción de este capı́tulo (apartado 8.2.1)
el pseudocódigo del programa se puede traducir a C usando un switch-case de la siguiente
manera:
70 CAPÍTULO 8. CONTROL DE FLUJO: SWITCH-CASE, BREAK Y CONTINUE

1 /* Programa: RobMan
2 *
3 * Descripción: Controla el robot de forma manual. Para ello espera a que
4 * el usuario introduzca su elección y ejecuta entonces la
5 * opción solicitada por el usuario.
6 *
7 * Revisión 0.0: 15/03/1998
8 *
9 * Autor: El robotijero loco.
10 */
11
12 #include <stdio.h>
13 #include <robot.h>
14
15 main(void)
16 {
17 int comando; /* Contendrá el comando introducido por el usuario */
18 float grados; /* Grados a girar por el robot*/
19
20 printf("Introduzca la opción: ");
21 scanf("%d", &comando);
22
23 switch(comando){
24 case 1:
25 avanza_1_cm_adelante();
26 break;
27 case 2:
28 printf("Introduzca el número de grados que desea girar: ");
29 scanf("%f", &grados);
30 gira(grados);
31 break;
32 default:
33 printf("Error: opción no existente\n");
34 break;
35 }
36 }
Ahora bien, salvo que el usuario tenga muy buena memoria, no se acordará para siempre
de los comandos, por lo que cada vez que tenga que ejecutar el programa tendrá que consultar
el manual de usuario para ver qué comandos tiene disponibles. Para evitar semejante engorro
la solución es bien sencilla: imprimamos al comienzo del programa una lista de los comandos
disponibles, junto con una breve descripción de lo que hacen. Haciendo esto, el programa al
arrancar podrı́a mostrar por pantalla lo siguiente:
Control manual del robot.

Menú de opciones:
1.- Avanzar 1 cm hacia adelante.
2.- Girar.

Introduzca opción:
8.2. LA CONSTRUCCIÓN SWITCH-CASE 71

A esto se le conoce con el nombre de menú de opciones, dado su parecido con los menús
de los restaurantes (aunque presentan el grave inconveniente de que no se pueden comer).
Para conseguir esto, basta con añadir al programa anterior entre sus lı́neas 19 y 20 las
instrucciones:
printf(" Control manual del robot.\n\n");
printf(" Menú de opciones:\n");
printf(" 1.- Avanzar 1 cm hacia adelante.\n");
printf(" 2.- Girar.\n\n");
Un ejemplo de sesión con el programa, suponiendo que dicho programa se llama robman,
serı́a:
u:\c\grupo00\robman> robman
Control manual del robot.

Menú de opciones:
1.- Avanzar 1 cm hacia adelante.
2.- Girar.

Introduzca opción: 1
u:\c\grupo00\robman> robman
Control manual del robot.

Menú de opciones:
1.- Avanzar 1 cm hacia adelante.
2.- Girar.

Introduzca opción: 2
Cuantos grados desea girar: 27
u:\c\grupo00\robman>
Como podemos apreciar el manejo del robot ası́ es un poco pesado, pues para cada instruc-
ción que deseemos darle hemos de volver a llamar al programa. Para hacer un poco más fácil
la vida del manejador del robot, podemos realizar un bucle que englobe a todo el programa
anterior. Un posible pseudocódigo de esta solución serı́a:

repetir{
imprimir menú
pedir comando al usuario;
si es 1:
avanza_1_cm_adelante();
si es 2:
pedir número de grados;
gira(número de grados);
}mientras el usuario quiera;

El problema ahora está en como descubrir cuando el usuario quiere abandonar el programa.
Obviamente la única manera de hacerlo (hasta que no se inventen los ordenadores que lean
el pensamiento) es preguntárselo directamente. Para ello nada más fácil que incluir una nueva
opción en el menú de forma que cuando se elija se cambie la variable lógica que controla el
bucle. Un posible pseudocódigo en el que se usa la opción 3 para salir del programa serı́a:
72 CAPÍTULO 8. CONTROL DE FLUJO: SWITCH-CASE, BREAK Y CONTINUE

salir = falso;
repetir{
imprimir menú
pedir comando al usuario;
si es 1:
avanza_1_cm_adelante();
si es 2:
pedir número de grados;
gira(número de grados);
si es 3:
salir = cierto;
}mientras salir sea falso;

Y el programa escrito en C quedarı́a como sigue:

1 /* Programa: RobMan
2 *
3 * Descripción: Controla el robot de forma manual. Para ello espera
4 * a que el usuario introduzca su elección y ejecuta
5 * entonces la opción solicitada por el usuario.
6 *
7 * Revisión 0.1: 16/03/1998
8 * Se imprime un menú de opciones y se introduce un
9 * bucle para repetir el programa.
10 *
11 * Revisión 0.0: 15/03/1998
12 *
13 * Autor: El robotijero loco.
14 */
15
16 #include <stdio.h>
17 #include <robot.h>
18
19 main(void)
20 {
21 int comando; /* Contendrá el comando introducido por el usuario */
22 float grados; /* Grados a girar por el robot*/
23 int salir; /* Variable lógica que pondremos a 1 cuando el usuario
24 desee salir del programa */
25
26 salir = 0; /* inicializamos la variable de control para que el
27 bucle se repita hasta que se pulse la opción 3.*/
28 do{
29 printf(" Control manual del robot.\n\n");
30 printf(" Menú de opciones:\n");
31 printf(" 1.- Avanzar 1 cm hacia adelante.\n");
32 printf(" 2.- Girar.\n");
33 printf(" 3.- Salir del programa.\n\n");
34 printf("Introduzca la opción: ");
35 scanf("%d", &comando);
8.3. LA SENTENCIA BREAK 73

36
37 switch(comando){
38 case 1:
39 avanza_1_cm_adelante();
40 break;
41 case 2:
42 printf("Introduzca el número de grados que desea girar: ");
43 scanf("%f", &grados);
44 gira(grados);
45 break;
46 case 3:
47 salir = 1;
48 break;
49 default:
50 printf("Error: opción no existente\n");
51 break;
52 }
53 }while(salir == 0);
54
55 printf("Que pase un buen dı́a.\n");
56 }

Ejercicios
1. Modificar el programa anterior para usar un bucle while en lugar del do-while.

2. Comprobar el funcionamiento del programa escrito en el ejercicio anterior en un or-


denador. Como no disponemos del robot ni de las funciones para manejarlo, sustituir
las llamadas a las funciones del robot por printfs que indiquen la llamada a la fun-
ción correspondiente para poder comprobar fácilmente el correcto funcionamiento de
nuestro programa.

8.3. La sentencia break


Esta sentencia hace que el flujo de programa salga del bloque en el que se encuentre. Este
bloque puede ser una construcción switch-case tal como acabamos de ver, o cualquiera de
los bucles que hemos visto en capı́tulos anteriores: for, while o do-while.

8.3.1. La sentencia break y el switch-case


En el caso de la construcción switch-case hemos visto que ha de usarse la sentencia
break explı́citamente para indicarle al programa que salga de la construcción. Esto que puede
parecer sin sentido, tiene su utilidad en ciertos casos en los que hay que ejecutar el mismo
conjunto de instrucciones para varios valores de la expresión que controla el switch-case.
Imaginemos que tenemos que realizar un programa con un menú para un usuario que es alérgi-
co a los números. Si se da esta situación la única opción que nos queda es usar un menú con
letras. En este caso serı́a deseable que el programa respondiese igual si se introduce la misma
letra en minúscula o en mayúscula, lo cual se resuelve fácilmente en C de la siguiente manera:

switch(letra){
74 CAPÍTULO 8. CONTROL DE FLUJO: SWITCH-CASE, BREAK Y CONTINUE

case ’A’:
case ’a’:
printf("Opción a del menú\n");
break;
case ’B’:
case ’b’:
printf("Opción b del menú\n");
break;
default:
printf("Opción no contemplada\n");
break;
}

En este ejemplo si letra vale ’A’ el programa salta a la instrucción que sigue a case ’A’:
y continuará ejecutándose hasta que encuentre un break o hasta que llegue el final del switch.
Por tanto si se da este caso (letra vale ’A’) se ejecutará printf("Opción a del menú\n")
y después se saldrá del switch. Conviene destacar que la sentencia case ’a’: es una etiqueta
del switch-case, no una instrucción ejecutable.

8.3.2. La sentencia break y los bucles


En ciertas ocasiones es necesario salir apresuradamente de un bucle, ya sea este for, while
o do-while, normalmente cuando se da alguna situación extraordinaria, situación que se de-
scubre normalmente mediante un if. Un esquema tı́pico de un bucle con un break es el
siguiente:

for(n=0; n<27; n++){


inst_1;
inst_2;
...
if(situacion_extraordinaria){
break;
}
inst_3;
...
}

En este ejemplo el bucle se repetirá 27 veces, salvo que se de la situación extraordinaria, en


cuyo caso se ejecutará la sentencia break situada dentro del if y se saldrá inmediatamente
del bucle, es decir, no se ejecutarán en esta iteración las instrucciones inst 3 y siguientes.
Conviene recalcar que en caso de que se salga del bucle con la instrucción break el valor de
la variable de control no será 27 como en una ejecución normal del bucle, sino menor.

8.3.3. Ejemplos
Tabla de logaritmos
Se desea imprimir una tabla de logaritmos desde un valor inicial hasta un valor final, en
pasos decrecientes de 0.1. El usuario introducirá el valor inicial y luego el valor final (menor
que el inicial). El programa imprimirá entonces la tabla de logaritmos. La solución obvia a
este problema es el uso de un bucle for desde el valor inicial hasta el final. El problema de
8.3. LA SENTENCIA BREAK 75

esta solución es que si el usuario no sabe mucho de matemáticas, puede introducir un valor
final negativo, con lo que el programa terminará de una forma poco amigable. Para evitar que
se calculen logaritmos de números menores o iguales a cero, podemos interrumpir el bucle
con una sentencia break si detectamos que el bucle llega a un número menor o igual a cero.
El programa escrito en C quedarı́a:
/* Programa: CalcLog
*
* Descripción: Calcula una tabla de logaritmos desde un valor inicial
* introducido por el usuario hasta otro final, también
* introducido por el usuario y menor que el anterior; en
* pasos de 0.1.
*
* Revisión 0.0: 24/03/1998
*
* Autor: El tablero aplicado.
*/

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

main(void)
{
double val_inic; /* Valor inicial */
double val_fin; /* Valor final */
double num; /* número usado para recorrer la tabla*/

printf("Valor Inicial = ");


scanf("%lf",&val_inic);

printf("Valor Final = ");


scanf("%lf",&val_fin);

/* imprimo una cabecera */


printf("\n numero logaritmo\n");

for(num = val_inic; num >= val_fin; num -= 0.1){


if(num <= 0.0){
break;
}
printf("%7.3f %7.3f\n", num, log(num) );
}
}
Un ejemplo de una sesión con el programa muestra su correcto funcionamiento:
Valor Incial = 1.01
Valor Final = -1.01

numero logaritmo
76 CAPÍTULO 8. CONTROL DE FLUJO: SWITCH-CASE, BREAK Y CONTINUE

1.010 0.010
0.910 -0.094
0.810 -0.211
0.710 -0.342
0.610 -0.494
0.510 -0.673
0.410 -0.892
0.310 -1.171
0.210 -1.561
0.110 -2.207
0.010 -4.605
En la que se aprecia que el bucle se interrumpe cuando num se hace igual a -0.09 gracias a
la sentencia break, terminando el programa.

8.4. La sentencia continue


En ciertos casos no es necesario ejecutar la parte final de una iteración dentro de un bucle
cuando se da una situación extraordinaria, situación que se descubre normalmente mediante
un if. En estos casos se puede utilizar la sentencia continue para volver al principio del bucle
y comenzar la siguiente iteración, saltándonos lo que nos quedaba de la iteración anterior. Un
esquema de este tipo de situaciones es el siguiente:
for(n=0; n<27; n++){
inst_1;
inst_2;
...
if(condicion_extraordinaria){
continue;
}
inst_3;
...
}
En este caso siempre que sea cierta la condición extraordinaria no se ejecutarán las in-
strucciones inst 3 y siguientes. Hay que destacar sin embargo que si se incrementará el valor
de n y se comparará su valor con 27 antes de proseguir con una nueva iteración del bucle.
El uso de la sentencia continue genera en la mayorı́a de los casos programas más difı́ciles
de leer que si se usa un if para ejecutar la parte final del bucle sólo cuando no sea cierta la
condición extraordinaria. Ası́ el programa anterior podrı́a escribirse de una manera mucho más
clara como:
for(n=0; n<27; n++){
inst_1;
inst_2;
...
if(!condicion_extraordinaria){
inst_3;
...
}
}
8.4. LA SENTENCIA CONTINUE 77

Como se puede observar el flujo del programa está mucho más claro en este caso, pues la
ejecución es siempre desde el principio del bucle hasta el final, sin saltos “raros” por el medio.

8.4.1. Ejemplos
Tabla de senos, cosenos, logaritmos
Se desea confeccionar una tabla de senos, cosenos y logaritmos (naturales y en base 10)
para los números comprendidos entre π y π espaciados a intervalos de 0.1.
Para confeccionar esta tabla lo natural es realizar un bucle for desde π hasta π que
calcule las funciones y las imprima. El problema que se nos presenta es que los logaritmos
no están definidos para números menores o iguales a cero. Una posible solución a este prob-
lema es colocar estas dos operaciones al final del bucle y usar la sentencia continue para
saltarnos el final del bucle siempre que el operando sea menor o igual que cero. Una posible
implantación del programa en C podrı́a ser:

/* Programa: CalcTabla
*
* Descripción: Calcula una tabla de senos, cosenos y logaritmos en
* base natural y decimal para los números comprendidos
* entre -pi y pi con intervalos de 0.1
*
* Revisión 0.0: 24/03/1998
*
* Autor: El tablero aplicado.
*/

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

main(void)
{
double num; /* número usado para recorrer la tabla*/

/* imprimo una cabecera */


printf("\n numero seno coseno logn log10\n");

for(num = -PI; num <= PI; num += 0.1){


/* el seno y el coseno se calculan siempre */
printf("%7.3f %7.3f %7.3f ", num, sin(num), cos(num) );

if(num <= 0.0){


printf("\n");/* si no calculo los logaritmos he de dar el salto
de carro que se darı́a en su printf */
continue;
}
printf("%7.3f %7.3f\n", log(num), log10(num));
}
}
78 CAPÍTULO 8. CONTROL DE FLUJO: SWITCH-CASE, BREAK Y CONTINUE

La salida del programa se muestra a continuación:


numero seno coseno logn log10
-3.142 -0.000 -1.000
-3.042 -0.100 -0.995
-2.942 -0.199 -0.980
-2.842 -0.296 -0.955
........................................
-0.342 -0.335 0.942
-0.242 -0.239 0.971
-0.142 -0.141 0.990
-0.042 -0.042 0.999
0.058 0.058 0.998 -2.840 -1.234
0.158 0.158 0.987 -1.843 -0.800
0.258 0.256 0.967 -1.353 -0.588
0.358 0.351 0.936 -1.026 -0.446
........................................
2.758 0.374 -0.927 1.015 0.441
2.858 0.279 -0.960 1.050 0.456
2.958 0.182 -0.983 1.085 0.471
3.058 0.083 -0.997 1.118 0.485
En donde se han eliminado lı́neas intermedias para ahorrar papel y no aburrir demasiado
al lector.
Este mismo ejemplo se podrı́a haber codificado sin usar continue de la siguiente manera:
/* Programa: CalcTabla
*
* Descripción: Calcula una tabla de senos, cosenos y logaritmos en
* base natural y decimal para los números comprendidos
* entre -pi y pi con intervalos de 0.1
*
* Revisión 0.1: 24/03/1998
* No se hace uso ahora de la sentencia continue.
* Revisión 0.0: 24/03/1998
*
* Autor: El tablero aplicado.
*/

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

main(void)
{
double num; /* número usado para recorrer la tabla*/

/* imprimo una cabecera */


printf("\n numero seno coseno logn log10\n");

for(num = -PI; num <= PI; num += 0.1){


8.5. EJERCICIOS 79

/* el seno y el coseno se calculan siempre */


printf("%7.3f %7.3f %7.3f ", num, sin(num), cos(num) );

if(num > 0.0){


printf("%7.3f %7.3f", log(num), log10(num));
}
printf("\n"); /* Imprimo el final de la linea de la tabla */
}
}

Que como se puede apreciar es más fácil de entender que el anterior. Nótese también que
la condición del if ahora es distinta. ¿Por qué?

8.5. Ejercicios
1. Modificar el programa CalcLog presentado en la sección 8.3.3 de forma que se com-
pruebe que el usuario introduce siempre un valor inicial mayor que el valor final, dando
un error en caso contrario y saliendo del programa.
2. Modificar el programa anterior para que cuando el usuario introduzca un valor erróneo
se le avise con un mensaje de error y se le vuelva a pedir de nuevo el valor final.
3. Realizar el programa para controlar el robot de forma interactiva presentado en la sec-
ción 8.2.3 pero usando un menú con letras, ya que el futuro usuario ha mostrado una
profunda alergia a los números, manifiesta con dolores de cabeza, congestión nasal y
escozores en la piel.

4. Escribir el programa anterior en el ordenador. ¿Funciona correctamente?. En caso de


que no funcione correctamente ¿Qué funcionamiento anómalo presenta?. Si el fun-
cionamiento anómalo es que después de introducir una opción nos la ejecuta y luego
sin que el usuario haga nada más, el programa dice el solito que se ha introducido una
opción errónea ¿a qué puede ser debido esto? (Pista: al introducir una opción después se
da al “intro”, que el ordenador interpreta como un carácter más llamado ’\n’). ¿Como
se podrı́a solucionar este problema tan desagradable? (Esto último es para nota).
80 CAPÍTULO 8. CONTROL DE FLUJO: SWITCH-CASE, BREAK Y CONTINUE
Capı́tulo 9

Vectores y Matrices

9.1. Introducción
En el capı́tulo 3 se han descrito los tipos de datos básicos que permiten declarar variables.
Pero estos tipos de datos sólo permiten construir variables que almacenen un único valor. En
este capı́tulo se ve cómo definir variables capaces de almacenar más de un valor, lo que nos
permite trabajar con mayor comodidad con grandes conjuntos de datos. Estas variables se
utilizan principalmente para trabajar con vectores, cadenas de caracteres y matrices.

9.2. Vectores
Un vector es un conjunto de datos del mismo tipo que se almacenan en el ordenador en
posiciones de memoria consecutivas y a los cuales se accede mediante un mismo nombre de
variable. La caracterı́stica fundamental es que todos los datos de un vector son del mismo tipo,
por ejemplo todos son int o todos son double.
La definición de vectores es parecida a la definición de variables, salvo que se debe especi-
ficar el tamaño del vector entre corchetes [ ]. Por ejemplo, para definir un vector llamado vec
que pueda almacenar 10 valores tipo double se escribe:

double vec[10];

La manera de acceder a los valores de estas variables es ahora diferente, ya que de todos
los elementos que componen el vector es necesario especificar cuál queremos modificar. Esta
especificación se realiza indicando entre corchetes el número de orden del elemento, teniendo
en cuenta que la numeración de los elementos siempre empieza en cero 1 .

vec[0]=54.23; /*Primer elemento del vector*/


vec=4.5; /* ERROR */
vec[10]=98.5; /* ERROR */

La segunda asignación no es correcta porque vec no es una variable tipo double sino un
vector y por lo tanto todas las asignaciones deben indicar el número de orden del elemento al
que queremos acceder. El vector vec del ejemplo tiene tamaño 10 porque fue declarado como
1 En matemáticas el primer elemento de los vectores suele ser el elemento 1 y por lo tanto en el vector a se habla

de a1 como primer elemento y en la matriz A se habla de A1 1

81
82 CAPÍTULO 9. VECTORES Y MATRICES

double vec[10] esto significa que almacena 10 elementos. Los 10 elementos se numeran
desde el 0 hasta el 9 y por lo tanto, el elemento 10 no existe y la tercera asignación tampoco
es válida. Es importante destacar que el compilador no dará error en el último caso ya que
no comprueba los rangos de los vectores. Este tipo de asignación hace que el valor 98.5 se
escriba fuera de la zona de memoria correspondiente al vector vec, y probablemente machaca
los valores de otras variables del programa. Hay que prestar especial atención ya que sólo
en algunos casos aparece un error de ejecución (generalmente cuando el ı́ndice del vector
es muy grande) pero en otras ocasiones no aparece ningún mensaje de error y el programa
simplemente funciona mal.
Como se puede sospechar, la mejor manera de trabajar con vectores es utilizando bucles
for. El siguiente ejemplo declara un vector de tamaño 10, lo inicializa dándole valores cre-
cientes y luego escribe todo el contenido del vector.

/*********************
Programa: vector.c
Descripción: Inicializa un vector de enteros y luego
escribe su contenido por pantalla.
*********************/

#include <stdio.h>

main(void)
{
double x[10];
int i;

/* inicialización */
for(i=0; i<10; i++) {
x[i]=2*i;
}

/* imprimir valores */
for(i=0; i<10; i++) {
printf("El elemento %d vale %4.1f\n",i,x[i]);
}
}

--- SALIDA --------------------


El elemento 0 vale 0.0
El elemento 1 vale 2.0
El elemento 2 vale 4.0
El elemento 3 vale 6.0
El elemento 4 vale 8.0
El elemento 5 vale 10.0
El elemento 6 vale 12.0
El elemento 7 vale 14.0
El elemento 8 vale 16.0
El elemento 9 vale 18.0
9.3. CADENAS DE CARACTERES 83

9.3. Cadenas de caracteres


Las cadenas de caracteres son conjuntos de letras que forman palabras o frases. Por ejem-
plo el nombre de una persona o su dirección se almacenan en forma de cadenas de caracteres
dentro de la memoria del ordenador. En el lenguaje de programación C no existe un tipo de
datos especı́fico para manejar cadenas de caracteres, sino que éstas se almacenan en vectores
de datos tipo char. Si por ejemplo queremos definir una variable capaz de almacenar nom-
bres de personas de hasta 10 caracteres de largo, tendremos que definir un vector de char de
longitud 10 de la manera siguiente:

char nombre[10];

Utilizando este tipo de definición puede deducirse que siempre se puede acceder a cualquier
carácter de la cadena de forma individual. Esto se hace especificando el ı́ndice del vector al
que queremos acceder. Un programa análogo al anterior, que inicializa e imprime un vector,
pero aplicado a cadenas de caracteres, es el siguiente:

/*********************
Programa: cadenas1.c
Descripción: Inicializa un vector de char y luego escribe
el contenido del vector carácter por carácter.
Revisión 0.0: 14/ABR/1998
Autor: Rafael Palacios
*********************/

#include <stdio.h>

main(void)
{
char nombre[10];
int i;

/* inicialización */
nombre[0]=’H’;
nombre[1]=’o’;
nombre[2]=’l’;
nombre[3]=’a’;

/* imprimir valores */
for(i=0; i<4; i++) {
printf("%c",nombre[i]);
}
printf("\n");
}

En el programa anterior sólo hay cuatro valores en la cadena de caracteres, por lo tanto el
número 4 se utiliza para controlar el final del bucle. A pesar de que la variable nombre tiene
espacio para almacenar 10 caracteres, serı́a un error poner el valor 10 como control de final de
bucle ya que los elementos quinto y siguientes no están inicializados. Si hubiésemos puesto el
valor 10 en lugar 4 en el bucle for la salida del programa podrı́a ser:
84 CAPÍTULO 9. VECTORES Y MATRICES

Hola!@#$%&

Las cadenas de caracteres suelen tener una longitud variable; es decir, aunque tengamos
memoria reservada para almacenar nombres de 10 caracteres unas veces tendremos nombres
largos y otras veces tendremos nombres cortos. A la hora de imprimir, copiar o comparar estos
nombres es necesario conocer su longitud.

9.3.1. Cadenas de caracteres de longitud variable


Básicamente existen dos métodos para manejar cadenas de caracteres de longitud variable
dentro de un ordenador. El primero es almacenar un valor entero que nos indique cuántas
letras están escritas realmente (en el ejemplo anterior este valor serı́a 4). Este método requiere
almacenar en algún lado el valor de la longitud lo que puede resultar algo incómodo. Una
solución serı́a utilizar el primer elemento del vector de char para almacenar la longitud, de esta
manera el vector almacena la longitud y los caracteres conjuntamente. El programa anterior
quedarı́a de la siguiente manera:

/*********************
Programa: cadenas2.c
Descripción: Inicializa un vector de char y luego escribe
el contenido del vector caracter por caracter.
Revisión 0.0: 14/ABR/1998
Autor: Rafael Palacios
*********************/
#include <stdio.h>

main(void)
{
char nombre[10];
int i;

/* inicialización */
nombre[0]=4;
nombre[1]=’H’;
nombre[2]=’o’;
nombre[3]=’l’;
nombre[4]=’a’;

/* imprimir valores */
for(i=1; i<=nombre[0]; i++) {
printf("%c",nombre[i]);
}
printf("\n");
}

Notar que el valor inicial y la condición del bucle for han cambiando.
Esta solución es válida aunque tiene dos inconvenientes fundamentales:

Se utiliza un carácter para guardar la longitud de la cadena y por lo tanto en lugar de 10


letras sólo podemos almacenar un máximo de 9.
9.3. CADENAS DE CARACTERES 85

La longitud máxima de la cadena de caracteres es 127, ya que el valor máximo que


puede almacenarse en la variable nombre[0] es 127 por ser de tipo char.
La segunda manera de manejar cadenas de caracteres de longitud variable es utilizar un
carácter especial para indicar el final de la cadena. Este es el método utilizando normalmente
en el lenguaje C y existen un montón de funciones dentro de la biblioteca estándar de C para
manejar cadenas de caracteres basadas en este método. Las caracterı́sticas de este método son:
En un vector de tamaño n sólo caben n 1 letras, ya que el carácter de final de cadena
ocupa una posición.
No existe lı́mite en la longitud de la cadena, el único lı́mite viene dado por el tamaño del
vector. Si se define un vector de tamaño 1000, como por ejemplo char nombre[1000],
se pueden almacenar sin ningún problema hasta 999 caracteres.
En cada posición de la cadena se puede almacenar cualquier carácter, a excepción del
carácter definido como final de cadena.
Dado que existe un carácter que indica el final de la cadena, no es posible almacenar en
ella cualquier tipo de información ya que dicho carácter no puede formar parte de la cadena.
Para evitar el mayor número de problemas, en el lenguaje C se ha elegido como carácter de
final de cadena el carácter número 0 de la tabla ASCII (también llamado NULL o carácter
nulo y representado mediante ’\0’). Existen muchos caracteres de uso poco frecuente, como
por ejemplo el carácter %, que podrı́an haberse utilizado para marcar el final de la cadena. Sin
embargo estos caracteres darı́an problemas antes o después. Si por ejemplo se desea realizar
un programa para imprimir octavillas con la frase “0,7 % YA” para pedir mayor justicia en este
mundo enfermo en el que el 20 % de la población de los paises del “norte” posee el 80 % de la
riqueza, el ordenador sólo imprmirı́a “0,7”, lo cual perturbarı́a un poco el mensaje original. El
carácter seleccionado en C, ’\0’, no da problemas porque nunca se utiliza como parte de una
cadena de caracteres, ya que no es un carácter imprimible. Usando esta técnica, el programa
anterior puede escribirse de la siguiente manera:

/*********************
Programa: cadenas3.c
Descripción: Inicializa un vector de char y luego escribe
el contenido del vector caracter por caracter hasta llegar
al caracter NULL.
Revisión 0.0: 14/ABR/1998
Autor: Rafael Palacios
*********************/
#include <stdio.h>

main(void)
{
char nombre[10];
int i;

/* inicialización */
nombre[0]=’H’;
nombre[1]=’o’;
nombre[2]=’l’;
nombre[3]=’a’;
86 CAPÍTULO 9. VECTORES Y MATRICES

nombre[4]=’\0’;

/* imprimir valores */
for(i=0; nombre[i]!=’\0’; i++) {
printf("%c",nombre[i]);
}
printf("\n");
}

Hay que destacar que este bucle for escribe los caracteres en pantalla siempre que éstos
sean sean diferentes del carácter NULL. Cuando el contador i alcance la posición en la que
está almacenado el carácter NULL, el bucle termina sin hacer el printf. Además este bucle
es válido incluso si la cadena de caracteres está vacı́a; es decir, si el primer carácter es direc-
tamente el carácter NULL entonces el bucle no arranca y no se escribe nada. Otra manera de
escribir este bucle, pero utilizando while en lugar de for es la siguiente:

i=0;
while(nombre[i] != NULL) {
printf("%c",nombre[i]);
i++;
}

Por haber elegido el carácter 0 como indicador de final de cadena, y gracias a que las condi-
ciones de los bucles sólo se consideran falsas cuando valen 0, la condición del while puede ser
el propio carácter nombre[i]. Lo mismo ocurre en el ejemplo del bucle for, donde la condi-
ción también puede ser nombre[i] en lugar de nombre[i]!=’\0’. Sin embargo el código
resultante queda menos claro.

9.3.2. Funciones estándar para manejar cadenas de caracteres


Existen una serie de funciones en la biblioteca estándar del lenguaje C que facilitan el
manejo de cadenas de caracteres, siempre que estas cadenas cumplan la norma de estar termi-
nadas con un carácter ’\0’. A continuación se muestran algunas de estas funciones, con una
breve explicación y un pequeño programa que simula su comportamiento. Es más interesante
comprender el funcionamiento de los programas que aprender lo que hace cada una de las
funciones (dado que esto figura en el manual del compilador). Entendiendo estos programas
no es difı́cil crear nuevas funciones especı́ficas que no se incluyen en la biblioteca estándar,
como por ejemplo funciones para cambiar minúsculas por mayúsculas.
Todas las funciones que se ven a continuación están definidas en el archivo string.h por
lo tanto hay que incluir este archivo al principio del programa para que el compilador pueda
comprobar que las estamos utilizando correctamente. Sin embargo todas estas funciones son
parte de la biblioteca estándar y no hace falta añadir ninguna biblioteca al enlazar el programa.

Longitud de una cadena: strlen(), string length


strlen calcula la longitud de una cadena de caracteres. Este cálculo se realiza recorriendo
la cadena hasta encontrar el carácter ’\0’.

/*********************
Programa: cadenas4.c
Descripción: Inicializa un vector de char y luego escribe
9.3. CADENAS DE CARACTERES 87

el contenido del vector carácter por carácter.


Utiliza la función strlen para obtener la longitud de
la cadena de caracteres.
Revisión 0.0:
*********************/

#include <stdio.h>
#include <string.h>

main(void)
{
char nombre[10];
int i;
int longitud;

/* inicialización */
nombre[0]=’H’;
nombre[1]=’o’;
nombre[2]=’l’;
nombre[3]=’a’;
nombre[4]=’\0’;

/* calculo la longitud */
longitud=strlen(nombre);

/* imprimir valores */
for(i=0; i<longitud; i++) {
printf("%c",nombre[i]);
}
printf("\n");
}

Nótese que en la llamada a strlen se pone directamente el nombre de la variable (sin


corchetes []).
Este programa es equivalente al siguiente, que no utiliza strlen.

/*********************
Programa: cadenas5.c
Descripción: Inicializa un vector de char y luego escribe
el contenido del vector caracter por caracter.
Calcula la longitud de la cadena de caracteres mediante
un bucle while.
Revisión 0.0: 14/ABR/1998
Autor: Rafael Palacios
*********************/
#include <stdio.h>

main(void)
{
char nombre[10];
88 CAPÍTULO 9. VECTORES Y MATRICES

int i;
int longitud;

/* inicialización */
nombre[0]=’H’;
nombre[1]=’o’;
nombre[2]=’l’;
nombre[3]=’a’;
nombre[4]=’\0’;

/* calculo la longitud */
longitud=0;
while(nombre[longitud] != 0) {
longitud++;
}

/* imprimir valores */
for(i=0; i<longitud; i++) {
printf("%c",nombre[i]);
}
printf("\n");
}

Copiar o inicializar: strcpy(), string copy


Esta función copia cadenas de caracteres. Es importante recordar que no es válido asignar
una constante a un vector y por lo tanto las siguientes instrucciones son incorrectas:

vec=4.5; /* Error si vec es vector*/


nombre="Pepito"; /* Error */

Esta función no verifica que el tamaño del vector de destino sea suficientemente grande
para guardar la cadena que se quiere copiar, por lo que el programador será responsable de
garantizar que esto ocurra para no producir fallos indeseables en el programa. El formato de
la llamada a strcpy es el siguiente:

strcpy(destino, origen);

es decir, tiene una sintaxis equivalente a la asignación normal de variables, del tipo:

destino=origen;

Esta función es muy útil para inicializar cadenas de caracteres, poniendo una constante en-
tre comillas dobles como argumento origen. Esta inicialización evita tener que definir carácter
por carácter el vector. Los siguientes principios de programa son equivalentes:

#include <string.h>
main(void) main(void)
{ {
char nombre[10]; char nombre[10];
9.3. CADENAS DE CARACTERES 89

/* inicialización */ /* inicialización */
nombre[0]=’H’; strcpy(nombre,"Hola");
nombre[1]=’o’;
nombre[2]=’l’;
nombre[3]=’a’;
nombre[4]=’\0’;
: :
: :
: :

Es importante destacar que la segunda versión incluye el archivo string.h para utilizar
la función strcpy y que no hace falta escribir el carácter ’\0’ porque está implı́cito en to-
das las cadenas de caracteres constantes (aquellas que se escriben directamente en el código
encerradas entre comillas).
El siguiente programa explica el comportamiento de la función strcpy para copiar la
variable a en la variable dir (ambas son vectores de char de tamaño suficiente).

for(i=0; a[i]!=’\0’; i++) {


dir[i]=a[i];
}
dir[i]=a[i]; /* Copio el NULL */

Este bucle for copia carácter a carácter la variable a en la variable dir, borrando la in-
formación que pudiese tener previamente. La última lı́nea de este código es fundamental, ya
que garantiza que la variable dir tenga carácter de final de cadena. Cuando a[0] vale ’\0’
el bucle no arranca y la última lı́nea hace dir[0]=a[0] por lo tanto copia el carácter NULL.
Cuando la cadena a no está vacı́a, el bucle copia carácter por carácter todas las letras hasta
llegar a NULL (carácter que no copia) entonces la última lı́nea copia el NULL en su sitio.

Comparación de cadenas: strcmp(), string compare


Esta función compara cadenas de caracteres. Principalmente se utiliza para ver si dos nom-
bres son iguales o para ordenarlos alfabéticamente. La función devuelve un valor de tipo int
que puede ser cero, negativo o positivo. Por ejemplo la comparación de las variables nombre1
y nombre2 puede dar los siguientes resultados:

i=strcmp(nombre1,nombre2);

La variable entera i tomará los siguientes valores:


i=0 si nombre1 es igual a nombre2. Esto significa que todos los caracteres del vector
nombre1 desde el ı́ndice 0 hasta el carácter NULL (incluyendo a éste) son iguales a los
correspondientes caracteres del vector nombre2. Pero puede ocurrir que el resto de los
caracteres (desde el carácter NULL hasta el final del vector) sean diferentes, lo cual da lo
mismo, ya que estos caracteres no forman parte de la cadena. También puede ocurrir que
los vectores sean de diferente tamaño, ya que sólo se comparan los caracteres anteriores
a NULL.
i<0 si nombre1 es menor que nombre2, entendiendo que nombre1 se ordena alfabética-
mente por delante de nombre2. En este caso hay que tener en cuenta que la ordenación
sigue los criterios de la tabla ASCII; es decir los números son menores que las letras, las
letras mayúsculas son menores que las minúsculas y las letras acentuadas son mayores
90 CAPÍTULO 9. VECTORES Y MATRICES

que cuaquier otro carácter normal. Por ejemplo: 3Com < Andalucı́a < Sevilla < boreal <
nórdico < África

i>0 si nombre1 es mayor que nombre2.

Un programa que tiene un funcionamiento equivalente a strcmp(a,b) es:

i=0;
resultado=0;
while(a[i]!=’\0’ && resultado==0) {
if (a[i]<b[i]) {
resultado=-1;
} else if (a[i]>b[i]) {
resultado=1;
}
i++;
}
if (resultado==0) { /* el while terminó porque a[i] vale ’\0’ */
if (b[i]!=’\0’) { /* la cadena b es más larga que a */
resultado=-1;
}
}

Ejercicios

1. Comprobar el funcionamiento del programa para distintos ejemplos. Comprobar si fun-


ciona para palabras como “fulmina” y “fulminante”, “alumno” y “alumna”, etc. ¿Da lo
mismo en qué variables (a o b) se almacenen las palabras?

Visualización de cadenas de caracteres

La función printf puede utilizarse para visualizar cadenas de caracteres, no hace falta
ninguna otra función especial. Cuando se pone el código %s dentro de la especificación de
formato, la función printf se encarga de escribir todos los caracteres del vector de char hasta
encontrar el carácter NULL.

i=0;
while(nombre[i]) {
printf("%c",nombre[i]);
}
printf("\n\n");

El programa anterior es equivalente a la instrucción:

printf("%s\n\n",nombre);

Nótese que ahora no se ponen corchetes, ya que queremos pasar a la función printf el vector
de char nombre completo, no uno de sus elementos.
9.3. CADENAS DE CARACTERES 91

Lectura de cadenas de caracteres desde teclado


La función scanf() puede utilizarse para leer cadenas de caracteres desde teclado uti-
lizando el formato %s, al igual que se hace con printf. Ejemplo:
/********
Programa: Lectura.c
Descripción: Lee una cadena de caracteres con scanf
Revisión 0.0
********/
#include <stdio.h>

main(void)
{
char a[100];

scanf("%s",a);
printf("Has escrito: %s\n",a);
}
Es importante tener en cuenta que el programa no puede saber cuántos caracteres va a
escribir el usuario y por lo tanto la variable a debe tener un tamaño suficientemente grande.
En este ejemplo se ha fijado el tamaño del vector a en 100 elementos, por lo tanto el usuario
puede escribir hasta 99 caracteres sin que falle el programa.
También es importante tener en cuenta que la función scanf realiza un manejo un poco
especial de los caracteres espacio (ASCII 32). La función scanf ignora todos los espacios que
preceden a una palabra y deja de leer del teclado cuando encuentra un espacio; es decir, sólo
vale para leer palabras sueltas. Una buena alternativa cuando se desea leer una frase completa
es utilizar la función gets().
La función gets() permite leer desde teclado cadenas de caracteres que contengan espa-
cios. Al leer una cadena de caracteres con gets todos los espacios que escriba el usuario se
almacenan en la cadena.
/********
Programa: Lectura2.c
Descripción: Lee una cadena de caracteres con gets
Revisión 0.0
********/
#include <stdio.h>

main(void)
{
char a[100];

gets(a);
printf("Has escrito: %s\n",a);
}

Ejercicios
1. Escribir un programa que lea una cadena con scanf y con gets, comparando los resul-
tados. Introducir una cantidad de caracteres mayor que la dimensión de la cadena y ver
92 CAPÍTULO 9. VECTORES Y MATRICES

si se produce algún resultado anómalo.

9.4. Matrices
La definición y utilización de matrices es análoga al caso de los vectores, salvo que ahora
tenemos dos dimensiones. Una matriz mat de tamaño 3x5 para números enteros se define
como:

int mat[3][5];

El acceso a los 15 elementos de esta matriz se realiza siempre utilizando dos ı́ndices que
también se escriben entre corchetes y que también empiezan por cero. Por lo tanto los elemen-
tos de la matriz mat tiene los siguientes nombres:

mat[0][0] mat[0][1] mat[0][2] mat[0][3] mat[0][4]


mat[1][0] mat[1][1] mat[1][2] mat[1][3] mat[1][4]
mat[2][0] mat[2][1] mat[2][2] mat[2][3] mat[2][4]

Al igual que en el caso de los vectores, no existen funciones especiales para imprimir ni
operar con matrices por lo que generalmente se utilizan bucles for. En el caso de las matrices
de dos dimensiones, como la del ejemplo anterior, se utilizan dos bucles for anidados para
recorrer todos los elementos. El siguiente programa define una matriz de 3x5 e inicializa a 7
todos sus elementos:

/*********************
Programa: matriz.c
Descripción: Inicializa a 7 todos los elementos de una matriz
de enteros de 3x5
Revisión 0.0: 14/ABR/1998
Autor: Rafael Palacios
*********************/

main(void)
{
int mat[3][5];
int i,j;

for(i=0; i<3; i++) {


for(j=0; j<5; j++) {
mat[i][j]=7;
}
}
}

9.5. Utilización de #define con vectores y matrices


Resulta muy útil definir los tamaños de vectores y matrices en función de parámetros,
ya que luego se pueden modificar fácilmente sin tener que revisar todo el programa. Estos
parámetros se definen con la instrucción #define que es una directiva del preprocesador, al
igual que #include.
9.5. UTILIZACIÓN DE #DEFINE CON VECTORES Y MATRICES 93

El programa anterior quedarı́a de la siguiente manera al utilizar un parámetro N para el


número de filas y un parámetro M para el número de columnas.

/*********************
Programa: matriz2.c
Descripción: Inicializa a 7 todos los elementos de una matriz
de enteros de N por M.
Se utiliza #define para definir el valor de N y M.
Revisión 0.0: 14/ABR/1998
Autor: Rafael Palacios
*********************/

#define N 3
#define M 5

main(void)
{
int mat[N][M];
int i,j;

for(i=0; i<N; i++) {


for(j=0; j<M; j++) {
mat[i][j]=7;
}
}
}

Si se quiere modificar el programa para que trabaje con matrices de tamaño 30x50 basta
con modificar las dos primeras lı́neas y volver a compilar. Todo el programa está escrito en
función de los dos parámetros, tanto el tamaño de las matrices como los lı́mites de los bucles,
y por lo tanto es válido para cualquier valor de N y M.
Ya se dijo en el capı́tulo 3 que los nombres de variables suelen escribirse en minúsculas,
para mayor claridad al leer el programa. Para diferenciar las variables de los parámetros, éstos
suelen escribirse en mayúsculas, como se ha hecho en el ejemplo.
En caso de querer preguntar al usuario el tamaño de las matrices con las que quiere cal-
cular, habrá que definir tamaños fijos suficientemente grandes (ya se verá más adelante cómo
se definen matrices y vectores de tamaño variable). Al utilizar tamaños fijos es imprescindible
comprobar, antes de iniciar los cálculos, que el valor introducido por el usuario no sea mayor
que el tamaño asignado para las matrices. El compilador no comprueba si se accede a elemen-
tos que se salen del vector por lo que el programa puede fallar estrepitosamente sin darnos
muchas pistas. El siguiente ejemplo es un programa para multiplicar una matriz por un vector.

/*
Programa: Matriz1
Descripción: Calcula el producto de una matriz por un vector
Revisión 0.1: mar/98
Autor: Rafael Palacios Hielscher
*/
#include <stdlib.h>
#include <stdio.h>
94 CAPÍTULO 9. VECTORES Y MATRICES

#define N 100
#define M 100

main(void)
{
double mat[N][M]; /* Matriz */
dobule vec[M]; /* vector */
int i; /* contador */
int j; /* otro contador (necesario para recorrer la matriz)*/
int n; /* número de filas de la matriz */
int m; /* número de columnas de la matriz, que es igual al
número de elementos del vector*/
double tmp; /* variable temporal */

printf("Número de filas? ");


scanf("%d",&n);
printf("Número de columnas? ");
scanf("%d",&m);
if (n>N || m>M) {
printf("Error, este programa no vale para "
"tamaños tan grandes.\n");
printf("Adiós\n");
exit(1);
}

/*** Lectura de datos ***/


/* matriz */
for(i=0; i<n; i++) {
for(j=0; j<m; j++) {
printf("mat[%d][%d]= ",i,j);
scanf("%lf",&mat[i][j]);
}
}
/* vector */
for(i=0; i<m; i++) {
printf("vec[%d]= ",i);
scanf("%lf",&vec[i]);
}

/*** Cálculos y Salida ***/


for(i=0; i<n; i++) {
tmp=0;
for(j=0; j<m; j++) {
tmp+=mat[i][j]*vec[j];
}
printf("Resultado[%d]=%f\n",i,tmp);
}
}
9.6. EJERCICIOS 95

9.6. Ejercicios
1. Escribir un programa que pregunte el tamaño de una matriz cuadrada, que pregunte
todos sus elementos y que calcule la traspuesta. Luego debe mostrar el resultado orde-
nadamente (en forma de matriz).
2. Basándose en el programa del producto de matriz por vector (programa Matriz1), es-
cribir un programa para calcular el producto de dos matrices a y b. El resultado debe
guardarse temporalmente en la matriz c y luego debe mostrarse por pantalla.
96 CAPÍTULO 9. VECTORES Y MATRICES
Capı́tulo 10

Funciones

10.1. Introducción
Una técnica muy empleada en la resolución de problemas es la conocida vulgarmente co-
mo “divide y vencerás”. Los programas de ordenador, salvo los más triviales, son problemas
cuya solución es muy compleja y a menudo dan auténticos dolores de cabeza al programador.
La manera más elegante de construir un programa es dividir la tarea a realizar en otras tareas
más simples. Si estas tareas más simples no son aún lo suficientemente sencillas, se vuelven
a dividir, procediendo ası́ hasta que cada tarea sea lo suficientemente simple como para re-
solverse con unas cuantas lı́neas de código. A esta metodologı́a de diseño se le conoce como
diseño de arriba-abajo, mundialmente conocido como top-down por aquello del inglés. Otras
ventajas de la división de un problema en módulos claramente definidos es que facilita el tra-
bajo en equipo y permite reutilizar módulos creados con anterioridad si éstos se han diseñado
de una manera generalista.
Esta metodologı́a se hace imprescindible hasta en los programas más sencillos; por ello,
todos los lenguajes de programación tienen soporte para realizar cómodamente esta división
en tareas simples. En el caso de C el mecanismo usado para dividir el programa en trozos son
las funciones (que en otros lenguajes de programación como el FORTRAN o el BASIC, son
llamadas subrutinas).
Una función en C consta de unos argumentos de entrada, una salida, y un conjunto de
instrucciones que definen su comportamiento. Esto permite aislar la función del resto del
programa, ya que la función puede considerarse como un “programa” aparte que toma sus
argumentos de entrada, realiza una serie de operaciones con ellos y genera una salida; todo
ello sin interactuar con el resto del programa.
Esta metodologı́a de diseño presenta numerosas ventajas, entre las que cabe destacar:

La complejidad de cada tarea es mucho menor que la de todo el programa, siendo abor-
dable.

Se puede repartir el trabajo entre varios programadores, encargándose cada uno de el-
los de una o varias funciones de las que se compone el programa. Esto permite cosas
tales como reducir el tiempo total de programación (si tenemos un ejército de progra-
madores), el que cada grupo de funciones las realice un especialista en el tema (por
ejemplo las de cálculo intensivo las puede realizar un matemático, las de contabilidad
un contable. . . ).

97
98 CAPÍTULO 10. FUNCIONES

Al ir construyendo el programa por módulos se pueden ir probando estos módulos con-


forme se van terminando, sin necesidad de esperar a que se termine el programa com-
pleto. Esto hace que, tanto la prueba de los módulos, como la corrección de los errores
cometidos en ellos, sea mucho más fácil al tener que abarcar solamente unas cuantas
lı́neas de código, en lugar de las miles que tendrá el programa completo.
Una vez construido el programa, también el uso de funciones permite depurar los prob-
lemas que aparezcan más fácilmente, pues una vez identificada la función que falla, sólo
hay que buscar el fallo dentro de dicha función y no por todo el programa.
Permite usar funciones creadas por otros programadores y almacenadas en bibliote-
cas. Esto ya se ha venido haciendo durante todo el curso. Funciones como printf(),
scanf(), strlen()... son funciones creadas por los programadores del compilador
y almacenadas en la biblioteca estándar de C. Además existen infinidad de bibliotecas,
muchas de ellas de dominio público, para la realización de tareas tan diversas como
cálculos matriciales, resolución de sistemas de ecuaciones diferenciales, bases de datos,
interfaz con el usuario. . .
Si se realizan lo suficientemente generales, las tareas se puede reutilizar en otros pro-
gramas. Ası́ por ejemplo, si en un programa es necesario convertir una cadena a mayúscu-
las y realizamos una función que realice dicha tarea, esta función se podrá usar en otros
programas sin ningún cambio. Si por el contrario la tarea de convertir a mayúsculas se
“incrusta” dentro del programa, su reutilización será muchı́simo más difı́cil.

10.2. Estructura de una función


Como se ha dicho en la introducción, una función tiene unos argumentos de entrada, un
valor de salida y una serie de instrucciones que forman el cuerpo de la función. Por ejemplo
una función que devuelve el cuadrado de su argumento se escribirı́a de la siguiente manera:

double Cuad(double x)
{
return x*x;
}

La sintaxis genérica de la definición de una función es la siguiente:

tipo devuelto NombreFunción(tipo 1 argumento 1,. . . ,tipo n argumento n)


{
instrucción 1;
instrucción 2;
...
instrucción n;
return expresión devuelta;
}

en donde:

tipo devuelto es el tipo del dato que devuelve la función. Si se omite, el compilador de C
supone que es un int; pero para evitar errores es importante especificar el tipo siempre.
Si la función no devuelve ningún valor ha de usarse el especificador de tipo void.
10.3. PROTOTIPO DE UNA FUNCIÓN 99

NombreFunción es el nombre de la función y será usado para llamar a la función desde


cualquier parte del programa. Este nombre tiene las mismas limitaciones que los nom-
bres de las variables discutidos en la sección 3.2. La única diferencia es que general-
mente los nombres compuestos suelen escribirse mezclando mayúsculas y minúsculas,
por ejemplo una función que borre la pantalla podrı́a llamarse BorrarPantalla() 1 .

tipo 1 argumento 1,. . . ,tipo n argumento n es la lista de argumentos que recibe la fun-
ción. Estos argumentos, también llamados parámetros2, pueden ser cualquier tipo de
dato de C: números enteros o reales, caracteres, vectores. . .

{ es la llave que marca el comienzo del cuerpo de la función.

instrucción 1. . . instrucción n son las instrucciones que componen el cuerpo de la fun-


ción y se escriben con un sangrado de algunos espacios para delimitar más claramente
dónde comienza y dónde termina la función.

return expresión devuelta; es la última instrucción de la función y hace que ésta ter-
mine y devuelva el resultado de la evaluación de expresión devuelta a quien la habı́a
llamado. Si el tipo devuelto de la función es void, se termina la función con la instruc-
ción return; (sin expresión devuelta). Sólo en este caso se puede omitir la instrucción
return, con lo cual la función termina al llegar a la llave }.

} es la llave que cierra el cuerpo de la función.

10.3. Prototipo de una función


En la sección anterior se ha visto cómo se define una función, pero para que una función
pueda usarse en otras partes del programa es necesario colocar al principio de éste lo que se
denomina el prototipo de la función. La misión de este prototipo es la de declarar la función
al resto del programa, lo cual permite al compilador comprobar que cada una de las llamadas
a la función es correcta, es decir, el compilador verifica:

Que los argumentos son correctos, tanto en número como en tipo, con lo que se evitan
errores como el olvido de un parámetro o suministrar un parámetro de un tipo erróneo.
En este último caso el prototipo permite realizar una conversión de tipos si ésta es posi-
ble. Por ejemplo si una función que tiene como argumento un double recibe un int se
realizará automáticamente la conversión de int a double. Si la conversión no es posi-
ble, se generará un error; ası́, si a una función que tiene como argumento una matriz
se le suministra en la llamada un entero el compilador generará un error, evitándose la
creación de un programa que falları́a estrepitosamente cuando se realizara la llamada
errónea.

Que el uso del valor devuelto por la función sea acorde con su tipo. Por ejemplo no se
puede asignar a una variable el valor devuelto por una función si la variable es de tipo
int y la función devuelve un vector.
1 Para distinguir a una función de una variable cuando se habla de ella, se termina su nombre con dos paréntesis,

tal como se acaba de hacer.


2 No confundir los parámetros de una función con los parámetros del programa, definidos mediante sentencias

#define.
100 CAPÍTULO 10. FUNCIONES

El uso de prototipos es opcional: el estándar de C sólo obliga a declarar las funciones


si éstas devuelven un tipo distinto de int. Sin embargo el uso de prototipos es extremada-
mente recomendable pues permite al compilador detectar errores que, de no existir el pro-
totipo, pasarı́an inadvertidamente al programa y éste falları́a estrepitosamente al intentar eje-
cutarlo. En un intento de ahorrar al alumno innumerables dolores de cabeza en las llamadas a
las funciones, en este curso cualquier función que aparezca sin su correspondiente prototipo
implicará una grave sanción para el alumno (materializable en bajada de puntos, multas, tir-
oncillos de orejas. . . )
La sintaxis del prototipo de una función es la siguiente:

tipo devuelto NombreFunción(tipo 1 argumento 1,. . . ,tipo n argumento n);

Que como puede observarse se corresponde con la primera lı́nea de la definición de la


función pero terminada en un punto y coma, con lo que los más avispados ya habrán adivinado
que la técnica del cortar y pegar será sumamente útil en la escritura de prototipos.

10.4. Ejemplo
Para aclarar ideas veamos un ejemplo sencillo que ilustra el uso de una función dentro de
un programa. Se necesita realizar un programa para calcular la suma de una serie de números
reales de la forma:
b  a 1 
∑n a     b
n a

La manera correcta de realizar la suma de la serie es definir una función para ello. Esto
permitirá aislar esta tarea del resto del programa, poder usar la función en otros programas y
todas las ventajas adicionales discutidas en la introducción.
La definición de la función se realizarı́a de la siguiente manera:

37 /* Función: SumaSerie()
38 *
39 * Descripción: Suma la serie aritmética comprendida entre sus
40 * argumentos a y b.
41 *
42 * Argumentos: int a: Valor inicial de la serie.
43 * int b: Valor final de la serie.
44 *
45 * Valor devuelto: int: Resultado de la serie.
46 *
47 * Revisión 0.1: 19/04/1998.
48 *
49 * Autor: El funcionario novato.
50 */
51
52 int SumaSerie(int a, int b)
53 {
54 int suma; /* variable para almacenar la suma parcial de la serie*/
55
56 suma = 0;
10.4. EJEMPLO 101

57
58 while(a<=b){
59 suma +=a;
60 a++;
61 }
62 return suma;
63 }

En primer lugar cabe destacar la ficha de la función. Es análoga a la ficha del programa
pero añadiendo la lista de argumentos y el valor devuelto. Esta ficha es la parte más importante
de la función de cara a su reutilización en otros programas, tanto por nosotros como por otros
programadores, pues sin necesidad de leer ni una sola lı́nea de código de la función podemos
saber la tarea que realiza, los datos que necesitamos suministrarle y el valor que nos devuelve.
Ni que decir tiene que en este curso la ausencia de la ficha en una definición de función
implicará castigos desmedidos contra el alumno.
A continuación de la ficha se encuentra la definición de la función, en la que apreciamos
en la primera lı́nea (52) el tipo devuelto, el nombre de la función y su lista de argumentos entre
paréntesis3 .
En la siguiente lı́nea se encuentra la llave { que delimita el comienzo del cuerpo de la
función.
A continuación, ya dentro del cuerpo de la función, se encuentran las definiciones de
las variables necesarias en el interior de la función. Estas variables se denominan variables
locales puesto que sólo son accesibles desde dentro de la función. En este ejemplo la variable
suma, declarada en la lı́nea 54, sólo es conocida dentro de la función y por tanto su valor sólo
puede ser leı́do o escrito desde dentro de la función, pero no desde fuera 4.
Después de las definiciones de las variables locales se encuentran las instrucciones nece-
sarias para que la función realice su tarea.
Cabe destacar que, tal como se aprecia en las lı́neas 58, 59 y 60, el uso de los parámetros
de la función dentro del cuerpo de ésta se realiza de la misma manera que el de las demás
variables.
Por último en la lı́nea 62 se usa la instrucción return para salir de la función devolviendo
el valor de suma.
Veamos a continuación el resto del programa para ilustrar el modo de llamar a una función.

1 /* Programa: SumaSerie
2 *
3 * Descripción: Calcula la suma de una serie aritmética entre un valor
4 * inicial y un valor final. Para ello se apoya en la
5 * función SumaSerie().
6 *
7 * Revisión 0.1: 19/04/1998
8 *
9 * Autor: El funcionario novato.
10 */
11
12 #include <stdio.h>
3 Nóteseque no se termina esta sentencia con un punto y coma
4 De hecho las variables locales se crean al entrar en la función y se destruyen al salir de ella, por lo que fı́sicamente
es imposible conocer o modificar sus valores fuera de la función porque las variables ni siquiera existen.
102 CAPÍTULO 10. FUNCIONES

13 #include <stdlib.h>
14
15 /* prototipos de las funciones */
16
17 int SumaSerie(int a, int b);
18
19 main(void)
20 {
21 int inicial; /* Valor inicial de la serie */
22 int final; /* Valor final de la serie */
23 int resultado; /* Resultado de la suma de la serie */
24
25 printf("Valor inicial de la serie: ");
26 scanf("%d", &inicial);
27
28 printf("Valor final de la serie: ");
29 scanf("%d", &final);
30
31 resultado = SumaSerie(inicial, final);
32 printf("El resultado es: %d\n", resultado);
33 }

En donde lo primero que cabe destacar es la inclusión del prototipo de la función al prin-
cipio del fichero (lı́nea 17). Esto permite que el compilador conozca el prototipo antes de que
ocurra cualquier llamada a la función SumaSerie() dentro del fichero y pueda comprobar que
las llamadas a dicha función son correctas (número de argumentos, tipos y valor devuelto).
Dentro de la función main() se piden al usuario los valores inicial y final de la serie y en la
lı́nea 31 se llama a la función SumaSerie(). Esta lı́nea ilustra dos aspectos muy importantes
de las funciones en C:

Una función que devuelve un tipo “t” se puede usar en cualquier expresión en donde
se puede usar una variable del mismo tipo “t”. En nuestro caso se puede introducir en
cualquier lugar en el que introducirı́amos un número o una variable de tipo int, como
por ejemplo en una asignación como ha sido el caso.

Los nombres de los argumentos en la llamada no tienen por qué ser iguales a los nom-
bres usados para dichos argumentos en la definición de la función. Esta caracterı́stica,
junto con la posibilidad de trabajar con variables locales, es la que permite reutilizar
fácilmente las funciones.

10.5. Paso de argumentos a las funciones. Variables locales


En el ejemplo anterior se acaba de ilustrar el mecanismo de llamada a una función. Sin
embargo al lector espabilado igual le ha surgido una duda: ¿que le ocurre a la variable inicial
que hemos pedido al usuario dentro de la función main()? Como se puede apreciar en la
lı́nea 60 del programa, dentro de la función SumaSerie() se incrementa el valor del primer
argumento, el cual se corresponde con la variable inicial dentro de main(); la pregunta
es si este incremento se realiza también sobre la variable inicial. La respuesta es que no.
Todas las variables de la función, tanto los argumentos (a y b) como las definidas localmente
(suma) son variables temporales que se crean cuando se llama a la función y que desaparecen
10.5. PASO DE ARGUMENTOS A LAS FUNCIONES. VARIABLES LOCALES 103

al salir de ésta. En la llamada a la función (lı́nea 31) los valores almacenados en las variables
inicial y final se copian en las variables temporales a y b con lo que cualquier operación
que se realice con estas copias (lecturas, escrituras, incrementos. . . ) no influirán para nada en
las variables originales.

Ejercicios

1. Para ilustrar lo que acabamos de discutir, introducir el programa anterior en el ordenador


y modificarlo para que se imprima en la pantalla el valor inicial de la serie, el valor final
y su suma. En primer lugar realizar la impresión desde main() y en segundo lugar desde
dentro de la función SumaSerie() observando las diferencias.

Esto que se acaba de discutir es uno de los aspectos más importantes de las funciones:
el hecho de que la función trabaje con sus propias variables la aisla del resto del programa,
pues se evita modificar de forma accidental una variable que no pertenezca a la función. Para
ilustrar este aspecto, imaginemos que el programa en el que hay que realizar la suma de la
serie descrita en el apartado anterior es mucho más largo y que como no hacemos nunca caso
del profesor, decidimos no usar funciones en el programa. Para sumar la serie se podrı́a hacer:

...
suma = 0;

while(inicial<=final){
suma +=inicial;
inicial++;
}
...

Los problemas que plantea esta solución son varios:

En primer lugar el valor inicial nos lo hemos cargado al ir incrementándolo, por lo que si
intentamos realizar la tarea propuesta en el ejercicio 1 nos daremos cuenta que el valor
inicial es igual que el final después de sumarse la serie.

Hemos usado una variable llamada suma para almacenar la suma parcial de la serie,
pero ¿que ocurre si en otra parte del programa se ha usado una variable también llamada
suma? Obviamente el valor que tuviese desaparece con el cálculo de la serie, y si se usa
después el programa fallará estrepitosamente y para colmo será difı́cil encontrar el error.

Si en otra parte se quiere calcular otra serie entre las variables ini y fin y queremos
aprovechar este trozo de código tendremos que cortar y pegar esta parte del programa en
la parte apropiada y luego cambiar inicial por ini y final por fin y tal vez suma por
cualquier otra cosa si queremos conservar el valor de la serie anterior. Además, si se nos
olvida cambiar alguna de las variables, el compilador no dará ningún mensaje de error
ya que todas las variables son válidas; pero sin embargo el resultado será incorrecto.

Como puede comprobar el alumno, las ventajas de usar funciones son muy numerosas y el
único inconveniente es aprender a usarlas, pero como esto va a ser imprescindible para aprobar
el curso, no sé a que estás esperando para empezar a practicar.
104 CAPÍTULO 10. FUNCIONES

10.6. Salida de una función y retorno de valores


Para salir de una función se usa la instrucción return seguida del valor que se desea
devolver. Su sintaxis es:

return expresión ;

En donde expresión es cualquier expresión válida de C que, una vez evaluada, da el valor
que se desea devolver. Si el tipo de dato de la expresión no coincide con el tipo de la función,
se realizarán las conversiones apropiadas siempre que éstas sean posibles o el compilador
generará un mensaje de error si dicha conversión no es posible.
La instrucción de salida puede aparecer en cualquier parte de la función, pudiendo existir
varias. Por ejemplo una función que devuelve el máximo de sus dos argumentos se puede
escribir como:

int Max(int a, int b)


{
if(a > b){
return a;
}else{
return b;
}
}

Sin embargo no conviene usar esta técnica, pues cualquier función es más fácil de entender
si siempre se sale de ella por el mismo sitio.
Si se desea realizar una función que no devuelva nada se debe usar el tipo void. Ası́ si
tenemos una función que no devuelve nada su declaración será:

void FuncionQueNoDevuelveNada(int arg1, double arg2);

y su definición serı́a:

void FuncionQueNoDevuelveNada(int arg1, double arg2)


{
...
}

En este tipo de funciones que no devuelven nada, la instrucción de salida es opcional,


terminando la función automáticamente cuando el flujo del programa alcanza la llave (}) que
cierra el cuerpo de la función. Si se desea poner explı́citamente la instrucción de salida o si se
desea salir desde otro punto de la función la sintaxis es:

return ;

dando el compilador un aviso en caso de que return esté seguido de alguna expresión. Del
mismo modo si una función que ha de devolver un valor no lo devuelve mediante la instrucción
return, el compilador dará un aviso.
10.7. FUNCIONES SIN ARGUMENTOS 105

10.7. Funciones sin argumentos


Al igual que se pueden definir funciones que no devuelven nada, también se pueden
definir funciones que no reciben ningún argumento. Un ejemplo de este tipo de funciones
es getchar(), disponible en la librerı́a estándar, la cual lee un carácter de la entrada estándar
y lo devuelve como un int. El prototipo de esta función es:

int getchar(void);

en donde podemos apreciar que al no recibir ningún argumento se ha puesto void como lista
de argumentos. En general una función que no toma ningún argumento se declara como:

tipo devuelto nombre función(void);

10.8. Ejemplos
Veamos a continuación algunos ejemplos más para aclarar ideas.

10.8.1. Elevar al cuadrado


Como en C no existe la operación para elevar un número a una potencia y la función pow()
es demasiado ineficiente para calcular un cuadrado, hemos decidido realizar una función que
devuelva el cuadrado de su argumento. Si trabajamos con números reales tanto el argumento
como el valor devuelto por la función serán de tipo double. La función, junto con un pequeño
programa que ilustra su uso, se muestra a continuación.

1 /* Programa: Cuadrados
2 *
3 * Descripción: Ejemplo de uso de la función cuadrado().
4 *
5 * Revisión 0.0: 15/04/1998
6 *
7 * Autor: El funcionario novato.
8 */
9
10 #include <stdio.h>
11
12 /* prototipos de las funciones */
13
14 double cuadrado(double a);
15
16 main(void)
17 {
18 double valor;
19
20 printf("Introduzca un valor ");
21 scanf("%lf",&valor);
22 printf("El cuadrado de %g es: %g\n", valor, cuadrado(valor));
23 }
106 CAPÍTULO 10. FUNCIONES

24
25 /* Función: cuadrado()
26 *
27 * Descripción: Devuelve el cuadrado de su argumento
28 *
29 * Argumentos: double a: Valor del que se calcula el cuadrado.
30 *
31 * Valor devuelto: double: El argumento a al cuadrado.
32 *
33 * Revisión 0.0: 15/04/1998.
34 *
35 * Autor: El funcionario novato.
36 */
37
38 double cuadrado(double a)
39 {
40 return a*a;
41 }

Nótese que la llamada a la función cuadrado() aparece ahora dentro de la lista de argu-
mentos de printf() (lı́nea 22). Tal como se dijo en la sección 10.4 una llamada a función
puede aparecer en cualquier lugar donde lo harı́a un objeto del mismo tipo que el devuelto por
la función. Como la función cuadrado es de tipo double puede aparecer en cualquier lugar en
el que pondrı́amos un double y ciertamente un argumento de printf() es un buen lugar para
un double.

Ejercicios
1. Modificar el programa anterior para calcular el cuadrado de un número entero. Modi-
ficar en primer lugar sólo el programa principal, de forma que se lea un número entero
y se llame a la misma función de antes y se imprima el resultado. ¿Funciona todo cor-
rectamente? ¿Que conversiones automáticas se están realizando?

2. Modificar ahora la función cuadrado para que calcule el cuadrado de un int e integrarla
en el programa anterior.

3. Usar la función cuadrado() diseñada en el ejercicio 2 en el ejemplo de los apuntes.


¿Funciona correctamente? ¿Que tipo de conversiones se realizan?

10.8.2. Factorial
Una función muy usada en estadı́stica que no esta disponible en la librerı́a matemática
estándar de C es el factorial. En este ejemplo vamos a realizar una función que calcula el
factorial de un número entero. La función junto con un programa que la usa se muestra a
continuación:

1 /* Programa: Factoriales
2 *
3 * Descripción: Ejemplo de uso de la función factorial().
4 *
5 * Revisión 0.0: 15/04/1998
10.8. EJEMPLOS 107

6 *
7 * Autor: El funcionario novato.
8 */
9
10 #include <stdio.h>
11
12 /* prototipos de las funciones */
13
14 long int factorial(long int a);
15
16 main(void)
17 {
18 long int valor;
19
20 printf("Introduzca un valor ");
21 scanf("%ld",&valor);
22 printf("El factorial de %ld es: %ld\n", valor, factorial(valor));
23 }
24
25 /* Función: factorial()
26 *
27 * Descripción: Devuelve el factorial de su argumento
28 *
29 * Argumentos: long int a: Valor del que se calcula el factorial.
30 *
31 * Valor devuelto: long int: El factorial del argumento a.
32 *
33 * Revisión 0.0: 15/04/1998.
34 *
35 * Autor: El funcionario novato.
36 */
37
38 long int factorial(long int a)
39 {
40 long int fact; /* valores parciales de factorial */
41
42 fact = 1;
43 while(a>0){
44 fact *= a;
45 a--;
46 }
47 return fact;
48 }

Ejercicios
1. Introducir el programa anterior en el ordenador y comprobar su funcionamiento para
valores positivos y negativos. ¿Da siempre resultados correctos?

2. Modificar la función factorial() para que devuelva -1 en caso de que no se pueda


108 CAPÍTULO 10. FUNCIONES

calcular el factorial de su argumento. Corregir el programa principal para que imprima


un mensaje de error si factorial() devuelve un -1.

10.9. Paso de vectores, cadenas y matrices a funciones


En ocasiones es necesario que una función trabaje con vectores o matrices (o ambas cosas).
Sin embargo, si cada vez que se llamase a una función se realizara una copia del vector o ma-
triz, el mecanismo de llamada serı́a tremendamente ineficiente en cuanto las matrices tuviesen
una longitud apreciable. Debido a esto, en C no se copia el vector o la matriz cuando se pasan
como argumento a una función, sino que se le dice en qué parte de la memoria está para que la
función trabaje sobre el vector o la matriz original. Esta solución aunque muy eficiente puede
ser muy peligrosa en manos de un programador inexperto, pues cada modificación que se real-
ice en el vector o la matriz desde dentro de la función modifica el vector o la matriz original,
por lo que se ha de ser extremadamente cuidadoso con el manejo de vectores y matrices dentro
de las funciones para evitar modificaciones no deseadas.

10.9.1. Paso de vectores


El prototipo de una función que acepta un vector como argumento es:

tipo devuelto NombreFunción(tipo nombre vector[] );

Es decir, se ponen dos corchetes [] a continuación del nombre del vector. Por ejemplo,
una función que calcule el producto escalar de dos vectores de tres dimensiones tendrá como
prototipo:

double ProdEscalar(double vect1[], double vect2[]);

Y la definición de la función, junto con un breve programa que ilustra su uso, es la sigu-
iente:
1 /* Programa: Producto Escalar
2 *
3 * Descripción: Ejemplo de uso de la función ProdEscalar().
4 *
5 * Revisión 0.0: 15/04/1998
6 *
7 * Autor: El funcionario novato.
8 */
9
10 #include <stdio.h>
11
12 /* prototipos de las funciones */
13
14 double ProdEscalar(double vect1[], double vect2[]);
15
16 main(void)
17 {
18 double vect1[3]={1, 2, 3};
19 double vect2[3]={3, 2, 1};
20
21 printf("El producto escalar vale %g\n", ProdEscalar(vect1, vect2));
10.9. PASO DE VECTORES, CADENAS Y MATRICES A FUNCIONES 109

22 }
23
24 /* Función: ProdEscalar()
25 *
26 * Descripción: Devuelve el producto escalar de dos vectores de 3 dimensiones
27 *
28 * Argumentos: double vect1[]: primer vector.
29 * double vect2[]: segundo vector.
30 *
31 * Valor devuelto: double: El producto escalar de los dos vectores
32 *
33 * Revisión 0.0: 15/04/1998.
34 *
35 * Autor: El funcionario novato.
36 */
37
38 double ProdEscalar(double vect1[], double vect2[])
39 {
40 int i;
41 double producto;
42
43 producto = 0;
44 for(i=0; i<3; i++){
45 producto += vect1[i]*vect2[i];
46 }
47
48 return producto;
49 }

En este ejemplo se han introducido dos novedades: La primera es la inicialización de los


vectores realizada en las lı́neas 18 y 19. Este tipo de inicialización de los vectores asigna al
primer elemento del vector el primer elemento de la lista entre llaves, al segundo elemento el
segundo valor y ası́ sucesivamente hasta terminar la lista. Ası́ pues la lı́nea 18 es equivalente a
las lı́neas:

vect1[0] = 1;
vect1[2] = 2;
vect1[3] = 3;

pero mucho más compacta como habrá observado el lector.


La segunda novedad es la manera de llamar a una función pasándole un vector. Esto se
ha mostrado en la lı́nea 21, en la que se llama a la función ProdEscalar(). Como puede
observar para pasar los vectores a la función se escribe su nombre sin corchetes en la lista de
argumentos: ProdEscalar(vect1, vect2).
Por último destacar que, aunque en el prototipo y en la definición de la función no se
especifica el tamaño de los vectores, dentro del cuerpo de la función se supone que son de
dimensión tres, pues el bucle que realiza el producto escalar comienza en la coordenada 0 y
termina en la 2, tal como se aprecia en las lı́neas 44 a 46. Esto es un grave inconveniente,
pues no hay manera de comprobar desde dentro de la función la dimensión de los vectores que
se han pasado como argumentos. Por tanto es necesario ser muy cuidadoso cuando se pasen
vectores o matrices a funciones y pasar siempre los vectores de la longitud adecuada.
Si se desea realizar una función que trabaje con vectores de cualquier dimensión habrá que
indicar de alguna manera el tamaño del vector, por ejemplo mediante un argumento adicional.
110 CAPÍTULO 10. FUNCIONES

Ası́, si se generaliza la función ProdEscalar() para trabajar con vectores de cualquier di-
mensión, el programa anterior queda:

1 /* Programa: Producto Escalar


2 *
3 * Descripción: Ejemplo de uso de la función ProdEscalar().
4 *
5 * Revisión 0.1: 20/04/1998
6 *
7 * Autor: El funcionario novato.
8 */
9
10 #include <stdio.h>
11
12 /* prototipos de las funciones */
13
14 double ProdEscalar(int dimension, double vect1[], double vect2[]);
15
16 main(void)
17 {
18 double vect1[3]={1, 2, 3};
19 double vect2[3]={3, 2, 1};
20
21 printf("El producto escalar vale %g\n", ProdEscalar(3, vect1, vect2));
22 }
23
24 /* Función: ProdEscalar()
25 *
26 * Descripción: Devuelve el producto escalar de dos vectores cuya
27 * dimensión viene dada por el primer argumento
28 *
29 * Argumentos: int dimensión: Dimensión de los vectores
30 * double vect1[]: primer vector.
31 * double vect2[]: segundo vector.
32 *
33 * Valor devuelto: double: El producto escalar de los dos vectores
34 *
35 * Revisión 1.0: 20/04/1998.
36 *
37 * Autor: El funcionario novato.
38 */
39
40 double ProdEscalar(int dimension, double vect1[], double vect2[])
41 {
42 int i;
43 double producto;
44
45 producto = 0;
46 for(i=0; i<dimension; i++){
47 producto += vect1[i]*vect2[i];
48 }
49
50 return producto;
51 }
10.9. PASO DE VECTORES, CADENAS Y MATRICES A FUNCIONES 111

10.9.2. Paso de cadenas a funciones


Todo lo que se acaba de discutir sobre el paso de vectores a funciones es aplicable al
paso de cadenas a funciones, pues como recordará las cadenas de caracteres no eran más que
vectores de tipo char.

10.9.3. Paso de matrices a funciones


El paso de matrices es un poco distinto al de los vectores. En ellas es necesario indicar
todas las dimensiones de la matriz salvo la primera. Por ejemplo si queremos realizar una
función para inicializar matrices de 3x5, el prototipo de la función será:

void InicializaMatriz(int matriz[][5]);

y su definición es:

void InicializaMatriz(int matriz[][5])


{
int n; /* ı́ndices para recorrer la matriz */
int m;

for(n=0; n<3; n++){


for(m=0; m<5; m++){
mat[n][m] = 0;
}
}
}

y para llamarla desde otra función se escribe el nombre de la matriz sin corchetes de la
misma forma que se hace con los vectores:

...
int mat[3][5];
...
InicializaMatriz(mat);
...

Por último cabe destacar dos cosas:

Que el compilador no realiza ningún tipo de comprobación sobre la coincidencia de


las dimensiones de la matriz que se le pasa a la función con las que ésta espera. Es
responsabilidad del programador que esto ocurra para evitar fallos catastróficos en el
programa.

Al contrario de lo que ocurrı́a con los vectores, no se pueden realizar funciones que
trabajen con matrices de cualquier dimensión, pues el compilador necesita saber las
dimensiones de la matriz para acceder a ella.

Ejercicios
1. Realizar una función para calcular el determinante de una matriz de 3 

3. El prototipo
de la función será:
112 CAPÍTULO 10. FUNCIONES

double Det(double mat[][3]);

2. Escribir una función para multiplicar dos matrices de 3 

3. El prototipo será:

void ProdMat(double A[][3], double B[][3], double Res[][3]);


Capı́tulo 11

Punteros

11.1. Introducción
Los punteros son un tipo de variable un poco especial, ya que en lugar de almacenar
valores (como las variables de tipo int o de tipo double) los punteros almacenan direcciones
de memoria. Utilizando variables normales sólo pueden modificarse valores, mientras que
utilizando punteros pueden manipularse direcciones de memoria o valores. Pero no se trata
simplemente de un nuevo tipo de dato que se utiliza ocasionalmente, sino que los punteros son
parte esencial del lenguaje C. Entre otras ventajas, los punteros permiten:

Definir vectores y matrices de tamaño variable, que utilizan sólo la cantidad de memoria
necesaria y cuyo tamaño puede reajustarse dinámicamente.

Definir estructuras de datos más completas como listas de datos o árboles.

Que las funciones devuelvan más de un valor.

11.2. Declaración e inicialización de punteros


Los punteros se declaran igual que las variables normales, pero con un asterisco (*) delante
del nombre de la variable. Por ejemplo:

int a;
int *pa;

En este ejemplo la variable a es de tipo entero y puede almacenar valores como 123 ó -24,
mientras que la variable p es un puntero y almacena direcciones de memoria. Aunque todos
los punteros almacenan direcciones de memoria, existen varios tipos de puntero dependiendo
de la declaración que se haga. Por ejemplo:

int *pti;
double *ptd;

Tanto pti como ptd son punteros y almacenan direcciones de memoria, por lo tanto alma-
cenan datos del mismo tipo y ocupan la misma cantidad de memoria, es decir, sizeof(pti)
== sizeof(ptd). La única diferencia es que el dato almacenado en la dirección de memoria
contenida en el puntero pti es un entero, mientras que el dato almacenado en la dirección

113
114 CAPÍTULO 11. PUNTEROS

Dirección Valor Variable Dirección Valor Variable

? C2B8 pti C2B8 123

? C37A ptd C37A 7.4e-3

Figura 11.1:

de memoria contenida en ptd es un double. Se dice por lo tanto que pti “apunta” a un en-
tero (puntero a entero) mientras que ptd “apunta” a un double (puntero a double). En la
figura 11.1 aparece un ejemplo en el que pti apunta a una dirección de memoria (0xC2B8)
donde se encuentra almacenado el valor entero 123, y ptd apunta a otra dirección de memoria
(0xC37A) donde se encuentra almacenado el valor 7.4e-3.

11.2.1. El operador unario &


Igual que las variables normales tienen un valor inicial desconocido después de su declaración,
también los punteros almacenan inicialmente una dirección desconocida; es decir, apuntan a
una dirección aleatoria que puede incluso no existir. Aunque se puede asignar un valor de
dirección a un puntero, esto no es nada aconsejable ya que serı́a una casualidad que en la
posición de memoria que se asigne exista un valor del tipo correspondiente al puntero. Si se
escribe:

int *pti;
pti=0xC2B8; /* ojo */

serı́a una casualidad que en la dirección de memoria 0xC2B8 hubiese un valor entero. Por lo
tanto en los próximos ejemplos vamos a hacer que nuestros punteros apunten a datos correctos
mediante la declaración de variables auxiliares. Esto es algo que no parece muy útil, pero vale
para explicar el funcionamiento de los punteros.
El operador & se utiliza para obtener la dirección de memoria de cualquier variable del
programa. Si nuestro programa tiene una variable entera que se llama ii, entonces podemos
obtener la dirección de memoria que el compilador ha preparado para almacenar su valor
escribiendo &ii.

int ii; /* variable entera */


int *pti; /* puntero a entero */

ii=78; /* inicializo ii */
pti=&ii; /* pti apunta a ii */

Al aplicar el operador & a la variable ii no se obtiene el valor entero almacenado en ella sino
la dirección de memoria en donde se encuentra dicho valor. Por lo tanto &ii es un puntero a
entero porque es una dirección de memoria donde se haya un entero. Suponiendo que el com-
pilador reserva la posición de memoria 0x34FF para la variable ii, la ejecución del programa
anterior llevarı́a a la organización de memoria que se muestra en la figura 11.2
11.2. DECLARACIÓN E INICIALIZACIÓN DE PUNTEROS 115

Dirección Valor Variable

? 34FF pti

34FF 78 ii

Figura 11.2:

El compilador dará un aviso si se intenta realizar una asignación en la que no corresponden


los tipos, por ejemplo al asignar &ii a un puntero a double, ya que &ii devuelve un puntero
a int. Este tipo de comprobaciones del compilador evita muchos errores de programación.
Por tanto hay que estar muy atento a los mensajes del compilador y hay que activar todos
los avisos. Asignaciones entre dos punteros también son válidas siempre que los dos punteros
sean del mismo tipo.
Por ejemplo, si se compila el programa siguiente:
1 #include <stdio.h>
2
3 main()
4 {
5 int ii;
6 double *pd;
7
8 ii = 27;
9
10 pd = &ii; /* ERROR */
11
12 printf("%lf\n", *pd*2);
13 }
el compilador (gcc en este caso1 ) generará el siguiente aviso:
tema11_2.c:10: warning: assignment from incompatible pointer type
donde se avisa que en la lı́nea 10 se asigna al puntero pd la dirección de un dato que no es
compatible con su tipo. A pesar de este aviso se genera un programa que si se ejecuta imprime
por pantalla el valor -3.997604 en lugar de 54.

Ejercicios
1. ¿Sabrı́a explicar por qué el programa anterior imprime un valor erróneo?

11.2.2. El operador unario *


Hasta ahora los punteros sólo han sido variables para guardar direcciones de memoria,
pero esto serı́a poco útil si no pudiésemos manipular el valor almacenado en dichas posiciones
1 Cualquier otro compilador generará un aviso parecido.
116 CAPÍTULO 11. PUNTEROS

Situación inicial Después de hacer *pti+=8; Después de hacer pti+=8;

Dirección Valor Variable Dirección Valor Variable Dirección Valor Variable

? 34FF pti ? 34FF pti ? 351F pti

34FF 78 ii 34FF 86 ii 34FF 86 ii

351F ????

Figura 11.3:

de memoria. El operador unario *, llamado operador de indirección, permite acceder al valor


por medio del puntero.

int ii; /* variable entera */


int *pti; /* puntero a entero */

pti=&ii; /* pti apunta a ii */


*pti=78; /* equivale a hacer ii=78; */

La asignación *pti=78 introduce el valor entero 78 en la posición de memoria almacenada


en el puntero pti. Como previamente se ha almacenado en pti la dirección de memoria
de la variable ii, la asignación equivale a dar el valor 78 directamente a la variable ii. Es
decir, tras la asignación pti=&ii disponemos de dos maneras de manipular los valores enteros
almacenados en la variable ii: directamente mediante ii o indirectamente mediante el puntero
pti. De momento esto no parece muy útil pero es importante que quede claro.

11.3. Operaciones con punteros


Una vez entendido lo que es un puntero veamos las operaciones que pueden realizarse
con estas variables tan especiales. En primer lugar ya se ha visto que admiten la operación
de asignación para hacer que apunten a un lugar coherente con su tipo. Pero también admite
operaciones de suma y diferencia que deben entenderse como cambios en la dirección a la
que apunta el puntero. Es decir, si tenemos un puntero a entero que almacena una determinada
dirección de memoria y le sumamos una cantidad, entonces cambiará la dirección de memoria
y el puntero quedará apuntando a otro sitio. Es importante diferenciar claramente las dos
formas de manipular el puntero:

*pti+=8;
pti+=8;

La primera lı́nea suma 8 al entero al que apunta pti y por lo tanto el puntero sigue apuntando
al mismo sitio. La segunda lı́nea suma 8 posiciones de memoria a la dirección almacenada en
el puntero, por lo tanto éste pasa a apuntar a otro sitio. (ver figura 11.3).
11.4. PUNTEROS Y VECTORES 117

De nuevo hacer que el puntero pase a apuntar a un lugar desconocido vuelve a ser peligroso
y habrá que tener cuidado con este tipo de operaciones. Es muy importante distinguir entre las
operaciones con punteros utilizando el operador * y sin utilizarlo.
Utilizando el operador * el puntero se convierte en una variable normal y por lo tan-
to admite todas las operaciones de estas variables. Por ejemplo ii=20 * *pti, *pti=a+b,
*pti+=7 etc. En operaciones normales de suma, resta, multiplicación, división, compara-
ciones... no hace falta ningún cuidado especial, sólo que el asterisco vaya unido al nombre
del puntero para que no se confunda con el operador de multiplicación. Las operaciones de
incremento y decremento puede dar lugar a confusión, por la mezcla de operadores, y se acon-
seja escribirlas siempre con paréntesis adicionales: ++(*p), (*p)++, --(*p) y (*p)--.
Sin utilizar el operador * se manipula la dirección del puntero y por lo tanto cambia el
lugar al que apunta. La operación más normal es incrementar la dirección en uno, con ello se
pasa a apuntar al dato que ocupa la siguiente posición de memoria. La aplicación fundamen-
tal de esta operación es recorrer posiciones de memoria consecutivas. Además de valer para
fisgonear la memoria del ordenador, ésto se utiliza para recorrer vectores (cuyos datos siem-
pre se almacenan en posiciones de memoria consecutivas). Pero antes de pasar al apartado de
vectores veamos cuanto se incrementa realmente un puntero.
Generalmente cada posición de memoria del ordenador puede almacenar un byte, por lo
tanto aumentando la dirección de un puntero en 1 pasarı́amos a apuntar al siguiente byte de
la memoria. Esto es algo bastante claro aunque difı́cil de manejar, porque como se ha visto
repetidamente a lo largo del curso, cada tipo de variable ocupa un tamaño de memoria difer-
ente. Por ejemplo los char sı́ ocupan 1 byte, pero los int ocupan 2 ó 4 bytes, los double
ocupan 8, etc. Además, estos tamaños dependen del ordenador y del compilador que se util-
ice. Para solucionar este problema, los punteros de cualquier tipo siempre apuntan al primer
byte de la codificación de cada dato. Cuando se accede al dato mediante el operador *, el
compilador se encarga de acceder a los bytes necesarios a partir de la dirección almacenada
en el puntero. En el caso de punteros a char sólo se accede al byte almacenado en la dirección
de memoria del puntero, mientras que el caso de punteros a double se accede a la posición
de memoria indicada en el puntero y a los 7 bytes siguientes. Todo este mecanismo ocurre de
manera automática y el programador sólo debe preocuparse de declarar punteros a char cuan-
do quiere trabajar con valores char o punteros a double cuando quiere trabajar con valores
double.
También para facilitar las cosas, la operación de sumar 1 a un puntero hace que su dirección
se incremente la cantidad necesaria para pasar a apuntar al siguiente dato del mismo tipo. Es
decir sólo en el caso de variables que ocupan 1 byte en memoria (variables char) la operación
de incremento aumenta en 1 la dirección de memoria, en los demás casos la aumenta más.
Lo que hay que recordar es que siempre se incrementa un dato completo y no hace falta
recordar cuánto ocupa cada dato en la memoria. Por ejemplo, si ptc es un puntero a char que
vale 0x3B20, la operación ptc++ hará que pase a valer 0x3B21. Por otro lado si ptd es un
puntero a double que vale 0x3B20, la operación ptd++ hará que pase a valer 0x3B28 (ver la
figura 11.4).

11.4. Punteros y vectores


Una de las aplicaciones más frecuentes de los punteros es el manejo de vectores y cadenas
de caracteres. Como se recordará todos los elementos de un vector se almacenan en posiciones
de memoria consecutivas y por lo tanto basta conocer la posición de memoria del primer
elemento para poder recorrer todo el vector con un puntero. El siguiente ejemplo inicializa
un vector de double por el método normal y luego escribe su contenido con la ayuda de un
118 CAPÍTULO 11. PUNTEROS

pti++

Dirección Valor Variable Dirección Valor Variable

? 3B20 pti 3B20 1 byte


3B21 1 byte

ptd++

Dirección Valor Variable Dirección Valor Variable

? 3B20 ptd 3B20

8 bytes

3B28

8 bytes

Figura 11.4:

puntero.

/*
Programa de punteros y vectores
Descripción: Este programa inicializa un vector y
luego escribe sus valores
Versión: 1.0
*/
#define N 10
#include <stdio.h>

main(void)
{
double a[N]; /* vector */
int i; /* contador */
double *pd; /* puntero a double */

/*** Inicialización ***/


for(i=0; i<N; i++) {
a[i]=7.8*i;
}

/*** Imprimir valores ***/


pd=&a[0]; /* Apunto al primer elemento del vector */
11.4. PUNTEROS Y VECTORES 119

for(i=0; i<N; i++) {


printf("%f\n",*pd); /* *pd es de tipo double */
pd++; /* pd pasa a apuntar al siguiente elemento */
}
}

Hay que tener en cuenta que cuando termina el programa el puntero pd queda apuntado a
un lugar no válido, ya que queda fuera del vector a.
Supongamos ahora que queremos copiar el vector a en el vector b de manera que sea su
inverso; es decir, si a vale [1,2,3,4] queremos que b valga [4,3,2,1]. Generar b a partir de a es
muy sencillo utilizando dos punteros pa y pb, uno creciente y otro decreciente:

pa=&a[0]; /* pa apunta al primer elemento del vector a */


pb=&b[N-1]; /* pb apunta al último elemento del vector b */
for(i=0; i<N; i++) {
*pb=*pa; /* copio un elemento */
pa++; /* avanzo un elemento */
pb--; /* retrocedo un elemento */
}

Con mucha frecuencia se utilizan varios punteros sobre el mismo vector para copiar datos
o hacer manipulaciones sin tener que andar con la complicación de manejar varios ı́ndices.
Los ejemplos más tı́picos son las transformaciones de cadenas de caracteres.

Ejercicios
1. Escribir un programa que obtenga la simetrı́a de una cadena de caracteres. Por ejemplo,
si la cadena vale "avión" el resultado debe ser "aviónnóiva".

11.4.1. Equivalencia de punteros y vectores


Cuando se define un vector, tal como se ha visto en el ejemplo anterior:

double a[N];

lo que ocurre es que se crea espacio en memoria para almacenar N elementos de tipo double
y se crea un puntero constante2, llamado a, que apunta al principio del bloque de memoria
reservada.
Por tanto para hacer que el puntero pd apunte al principio de la matriz a se puede hacer
pd=&a[0] o simplemente pd=a.

El operador [ ]
Cuando se accede a un elemento de la matriz mediante:

a[3]=2.7;

el programa toma el puntero constante a, le suma el valor que hay escrito entre los corchetes
(3) y escribe en dicha dirección el valor 2.7. Es decir, la sentencia anterior es equivalente a
escribir:
2 El programa no puede cambiar la dirección almacenada en un puntero constante. Por ejemplo a=&ii es ilegal
120 CAPÍTULO 11. PUNTEROS

*(a+3)=2.7;

Además este operador se puede aplicar sobre cualquier puntero, no sólo sobre los punteros
constantes, por lo que los dos trozos de código que siguen son equivalentes:

pd=a;
a[3]=2.7; pd[3]=2.7;

A modo de resumen, si tenemos un vector vec de 100 elementos y queremos asignar el


valor 45 al 5o elemento, pondremos:

vec[4]=45;

En caso de tener un puntero punt que apunta al primer elemento de vector, podemos realizar
la misma asignación de tres maneras:

Moviendo el puntero y luego volviendo atrás

punt+=4;
*punt=45;
punt-=4;

Calculando la dirección directamente

*(punt+4)=45;

Utilizando corchetes, que es lo más cómodo.

punt[4]=45;

11.5. Punteros y funciones


Hasta ahora hemos visto que los argumentos de una función no se modifican, ya que la
función trabaja sobre copias de los mismos. Esto es útil y muy seguro la mayorı́a de las veces,
pero en otras ocasiones nos puede interesar modificar realmente el valor de las variables. Por
ejemplo la siguiente función para cambiar el valor de dos variables no hace nada:

/* Función: Cambio()
Descripción: Intenta cambiar el valor de dos variables
Comentario: No lo consigue, hacen falta punteros.
Revisión: 1.0
Autor: Novato
*/
void cambio(int a, int b)
{
int tmp;

tmp=a;
a=b;
b=tmp;
}
11.5. PUNTEROS Y FUNCIONES 121

Esta función no hace nada porque cambia los valores de a y b, pero éstos son copias de
las variables que se utilizan al llamar a la función. Si la llamada es cambio(x,y) entonces a
es una copia de x y b es una copia de y. Aunque la función cambia los valores de a y b, al
terminar y volver al programa principal, los valores de x e y no se ven alterados.
La manera de conseguir que la función cambio sea útil es trabajar con punteros. La versión
1.1 de esta función utiliza como argumentos dos punteros en lugar de dos enteros; el nuevo
prototipo es:
void cambio(int *pa, int *pb);
Al recibir las direcciones de las dos variables, sı́ tenemos acceso a los valores originales 3
y por lo tanto se puede hacer el cambio. Ahora se trata de cambiar los valores contenidos en
las direcciones apuntadas por los punteros pa y pb.
/* Función: Cambio()
Descripción: Intenta cambiar el valor de dos variables
Comentario: OK
Revisión: 1.1
Autor: El que sabe de punteros
*/
void cambio(int *pa, int *pb)
{
int tmp;

tmp=*pa;
*pa=*pb;
*pb=tmp;
}
La única duda serı́a ¿Cómo llamamos a la función para darle las direcciones en lugar de los
valores? La respuesta ya se ha visto porque el operador & nos facilita la dirección de cualquier
variable. En este caso la llamada para cambiar los valores de las variables x e y es la siguiente:
cambio(&x, &y);
De hecho, llevamos utilizando llamadas de este estilo desde que se mencionó por primera
vez la función scanf. ¿Por qué en la función scanf se escriben las variables con un & mien-
tras que en printf se escriben sin nada? La respuesta es sencilla después de haber entendido
los punteros y las funciones. La función printf se encarga de escribir el valor de nuestras
variables y le da igual tener acceso directo al valor o trabajar sobre una copia; para mayor
facilidad de programación (y para mayor seguridad) se opta por pasarle una copia. Sin embar-
go la función scanf lee un valor del teclado y lo escribe dentro de una de las variables de la
función que la llama. Por lo tanto necesita acceso directo a la dirección de memoria donde se
almacena el valor de la variable.
Y la última pregunta es ¿Por qué es tan importante especificar correctamente el formato de
las variables en scanf? Porque si el formato dice %f la función scanf escribe en total 4 bytes
(sizeof(float)) a partir de la dirección de memoria, pero si el formato dice %lf entonces
scanf escribe 8 bytes. Cuando la variable no coincide con el tipo especificado pueden ocurrir
dos cosas: que se escribe fuera de las posiciones de memoria reservadas para la variable (prob-
ablemente machacando la variable de al lado) o que no se escribe suficiente (dejando parte de
la variable sin inicializar).
3 Usando el operador de indirección * sobre el puntero.
122 CAPÍTULO 11. PUNTEROS

11.5.1. Retorno de más de un valor por parte de una función


Como se ha visto anteriormente una función sólo puede devolver un valor mediante la
sentencia return. Sin embargo en muchas ocasiones es deseable que una función devuelva
más de un valor. En estos casos la técnica usada consiste en pasarle a la función la dirección
de las variables donde queremos que nos devuelva sus resultados; de modo que la función
pueda modificar directamente el valor de dichas variables. Por ejemplo, si queremos realizar
una función que devuelva la suma y el producto de dos valores, dicha función se escribirı́a:
void SumaProd(double *psuma, double *pprod, double dato1, double dato2)
{
*psuma = dato1 + dato2;
*pprod = dato1 * dato2;
}
y la manera de llamarla para que devuelva la suma y el producto de 2 y 3 en las variables sum
y prod serı́a:
SumaProd(&sum, &prod, 2.0, 3.0);
Por convenio, en estos casos se suelen colocar en la lista de argumentos las variables en
las que devuelve la función sus resultados en primer lugar.

11.6. Asignación dinámica de memoria


Existen infinidad de aplicaciones en las cuales no se conoce la cantidad de memoria nece-
saria para almacenar los datos hasta que no se ejecuta el programa. Si por ejemplo se desea
escribir un programa que calcule el producto escalar de dos vectores, la dimensión de éstos
no se conoce hasta que no se le pregunta al usuario al comienzo del programa. En este tipo de
situaciones las posibles soluciones son dos:
Crear vectores de un tamaño fijo lo suficientemente grande.
Crear el vector dinámicamente al ejecutarse el programa.
La primera solución presenta dos inconvenientes: el primero es que si el usuario necesita
calcular el producto escalar de dos vectores de dimensión mayor a la dimensión máxima que se
eligió al escribir el programa, tendrá que llamarnos para que incrementemos dicha dimensión
máxima (lo cual puede estar bien para cobrarle soporte técnico, pero dará mucho que hablar
sobre nuestra habilidad como programadores). El segundo inconveniente es que normalmente
se está desperdiciando una gran cantidad de memoria en definir un vector demasiado grande
del cual sólo se va a utilizar una pequeña parte en la mayorı́a de los casos. En esta situación,
dado que todos los sistemas operativos actuales son multitarea, si un programa usa toda la
memoria del ordenador no se podrán arrancar otros programas.
Estos dos inconvenientes se solucionan con el uso de memoria dinámica: mediante esta
técnica, una vez que se está ejecutando el programa y se conoce la cantidad de memoria que
se necesita, se realiza una llamada al sistema operativo para solicitarle un bloque de memoria
libre del tamaño adecuado. Si queda memoria, el sistema operativo nos devolverá un puntero
que apunta al comienzo de dicho bloque. Este puntero nos permite acceder a la memoria a
nuestro antojo, siempre y cuando no nos salgamos de los lı́mites del bloque 4. Una vez que
4
Si intentamos leer o escribir fuera del bloque de memoria que se nos ha asignado, los resultados pueden ser
desastrosos en función de la zona de memoria a la que accedamos erróneamente. Por tanto hay que ser muy cuidadosos
cuando se usan estas técnicas.
11.6. ASIGNACIÓN DINÁMICA DE MEMORIA 123

se termina de usar la memoria, esta debe liberarse al sistema operativo para que los demás
programas puedan usarla.
Esta técnica permite por tanto un manejo más racional de la memoria, aparte de permi-
tirnos crear estructuras de datos más complejas como listas enlazadas y árboles. Sin embargo
estos temas quedan fuera del alcance de este curso.

11.6.1. Las funciones calloc() y malloc()


Estas dos funciones permiten al programa solicitar al sistema operativo un bloque de
memoria de un tamaño dado. Ambas funciones devuelven un puntero al principio del bloque
solicitado o NULL si no hay suficiente memoria. Es muy importante por tanto verificar siempre
que se solicite memoria al sistema operativo que éste nos devuelve un puntero válido y no un
NULL para indicarnos que no tiene tanta memoria disponible.
Los prototipos de ambas funciones son:

void *calloc(size_t numero elementos, size_t tamaño elemento);


void *malloc(size_t tamaño bloque);

y se encuentran en el archivo cabecera stdlib.h que habrá de ser incluido al principio del
fichero para que el compilador pueda comprobar que las llamadas se realizan correctamente.
Por lo demás, como ambas funciones pertenecen a la librerı́a estándar, no hace falta añadir
ninguna librerı́a adicional en el enlazado.
La función calloc() reserva un bloque de memoria para un numero elementos de tama ño elemento,
inicializa con ceros el bloque de memoria y devuelve un puntero genérico (void *) que apunta
al principio del bloque o NULL en caso de que no exista suficiente memoria libre.
La función malloc() reserva un bloque de memoria de tamaño bloque (medido en bytes)
y devuelve un puntero al principio del bloque o NULL en caso de que no exista suficiente
memoria libre.
Por ejemplo para crear un vector de 100 enteros se podrı́a realizar lo siguiente:

int *pent; /* puntero al principio de la vector */


...
pent = (int *) calloc(100, sizeof(int));
if(pent == NULL){ /* Si no hay memoria */
printf("Error: No hay suficiente memoria\n");
exit(1);
}
/* Si hay memoria el programa continúa y podemos usar el vector
recién construido */
pent[0] = 27; /* recuérdese la equivalencia entre punteros y vectores */
...

La petición de memoria del programa anterior podrı́a haberse realizado también de la


siguiente manera5:

pent = (int *) malloc(100*sizeof(int));

En ambas llamadas se reserva un bloque de 100 enteros y se devuelve un puntero al prin-


cipio de dicho bloque. Nótese que en el prototipo de estas dos funciones se especifica que se
5 Aunque en este caso la memoria no se inicializarı́a con ceros.
124 CAPÍTULO 11. PUNTEROS

devuelve un puntero de tipo void. Este tipo es un puntero genérico que ha de convertirse me-
diante un molde (cast) al tipo de puntero con el que vamos a manejar el bloque. En el ejemplo,
como se deseaba crear un vector de 100 enteros, el puntero devuelto se ha convertido en un
puntero a entero mediante el molde (int *).
Por último conviene destacar que en las llamadas a las funciones calloc() y malloc() se
ha usado sizeof(int) para especificar la cantidad de memoria que se necesita. Esto es muy
importante de cara a la portabilidad del programa. Si aprovechando que conocemos el tamaño
de un int en un PC con GCC hubiésemos puesto un 4 en lugar de sizeof(int), el programa
no funcionarı́a bien si lo compilamos en Borland C para MS-DOS, el cual usa enteros de 2
bytes.

11.6.2. La función free()


Una vez que se ha terminado de usar la memoria es necesario liberarla para que quede
disponible para los demás programas. El no hacerlo hará que cada vez que se ejecute nuestro
programa “desaparezca” un trozo de memoria que no se recuperará hasta que reiniciemos el
equipo6. La función free() libera la memoria previamente asignada mediante calloc() o
malloc(). Se ha recalcado lo de previamente asignada porque liberar memoria que no ha sido
asignada7 es un grave error que puede dejar “colgados” a algunos sistemas operativos.
El prototipo de esta función es:

void free(void *puntero al bloque);

que también está en el archivo cabecera stdlib.h. Esta función, al igual que sus compañeras
de faenas: calloc() y malloc(), se encuentra en la librerı́a estándar.
La función simplemente libera el bloque a cuyo principio apunta el puntero al bloque.
Siguiendo con el ejemplo anterior para liberar la memoria previamente asignada habrı́a
que realizar:

free(pent);

Hay que destacar que el puntero al bloque ha de apuntar exactamente al principio del
bloque, es decir, ha de ser el puntero devuelto por la llamada a la función de petición de
memoria (calloc() o malloc()). Por ejemplo, el siguiente trozo de código hará que el pro-
grama aborte al hacer la llamada a free() o si el sistema operativo no es muy espabilado lo
dejará incluso colgado (se recomienda no probarlo en el examen):

int *pent;

pent = calloc(100, sizeof(int));


if(pent == NULL){
printf("Error: Se ha gastado la memoria. Compre más.\n");
exit(1);
}
...
pent++; /* peligro!! */
...
free(pent); /* Error catastrófico */
6 Salvo que el sistema operativo sea muy astuto.
7O liberarla varias veces, que es el error más habitual.
11.6. ASIGNACIÓN DINÁMICA DE MEMORIA 125

También es un grave error el seguir usando la memoria una vez liberada, pues aunque
tenemos un puntero que apunta al principio del bloque, este bloque ya no pertenece a nuestro
programa y por tanto puede estar siendo usado por otro programa (o por nuestro programa
si se ha solicitado más memoria), por lo que cualquier lectura nos puede devolver un valor
distinto del escrito y cualquier escritura puede modificar datos de otro programa (o de otro
bloque del nuestro), tal como se muestra en el siguiente ejemplo:

int *pent; /* puntero al principio de la vector */


int a; /* Variable auxiliar */

pent = (int *) calloc(100, sizeof(int));


if(pent == NULL){ /* Si no hay memoria */
printf("Error: No hay suficiente memoria\n");
exit(1);
}

pent[0] = 27; /* recuérdese la equivalencia entre punteros y vectores */

free(pent);

a = pent[0]; /* ERROR! El bloque al que apunta pent ya no pertenece a


nuestro programa. Por tanto puede que en a no se
escriba 27 */
pent[0] = 40; /* ERROR! Estamos modificando memoria que ya no es
nuestra y los resultados pueden ser catastróficos */

11.6.3. Ejemplo
Para fijar ideas vamos a ver a continuación un programa en el que se hace uso de asignación
dinámica de memoria.
Se ha de escribir un programa que pida un vector al usuario y una vez que éste lo haya
introducido, ha de generar otro vector que contenga sólo los elementos del primer vector que
sean números pares e imprimirlos en la pantalla. Por supuesto la dimensión de ambos vectores
es desconocida antes de ejecutar el programa.
Como no se conoce la dimensión de ninguna de los vectores y sabemos ya un montón acer-
ca de asignación dinámica de memoria decidimos, en lugar de crear dos vectores enormes que
valgan para cualquier caso, crear los vectores del tamaño justo cuando se ejecute el programa.
Un pseudocódigo del programa se muestra a continuación:

Pedir dimensión del vector al usuario


Pedir memoria para el vector
Pedir al usuario cada uno de los elementos del vector
Contar el número de elementos pares del vector
Pedir memoria para el vector de números pares.
Copiar los números pares del primer vector al segundo
Imprimir el segundo vector

El programa completo se muestra a continuación:


1 /* Programa: BuscaPar
2 *
126 CAPÍTULO 11. PUNTEROS

3 * Descripción: Pide al usuario un vector de enteros y genera otro que


4 * contiene sólo los elementos pares de dicho vector,
5 * imprimiéndolo antes de terminar.
6 *
7 * Revisión 0.0: 19/05/1998
8 *
9 * Autor: El programador dinámico.
10 */
11
12 #include <stdio.h>
13 #include <stdlib.h>
14
15 main(void)
16 {
17 int *pvec_usu; /* Puntero al vector introducido por el usuario (creado
18 dinámicamente) */
19 int *pvec_par; /* Puntero al vector de elementos pares (creado
20 dinámicamente)*/
21 int dim_usu; /* Dimensión del vector del usuario */
22 int dim_par; /* Dimensión del vector de elementos pares */
23 int n; /* Índice para los for */
24 int m; /* Índice para recorrer la matriz de elementos pares */
25
26 printf("Introduzca la dimensión del vector: ");
27 scanf("%d",&dim_usu);
28
29 /* Asignamos memoria para el vector del usuario. Contiene dim_usu enteros */
30
31 pvec_usu = (int *) calloc(dim_usu, sizeof(int));
32
33 if(pvec_usu == NULL){ /* Estamos sin memoria */
34 printf("Error: no hay suficiente memoria para un vector de %d elementos\n",
35 dim_usu);
36 exit(1);
37 }
38
39 /* Pedimos los elementos del vector */
40
41 for(n=0; n<dim_usu; n++){
42 printf("Elemento %d = ", n);
43 scanf("%d", &(pvec_usu[n]) );
44 }
45
46 /* Contamos los pares en la variable dim_par */
47 dim_par = 0; /* De momento no hay ningún elemento par */
48 for(n=0; n<dim_usu; n++){
49 if( (pvec_usu[n]%2) == 0 ){ /* es par */
50 dim_par ++;
51 }
52 }
53
54 /* Se asigna memoria para los números pares */
55
56 pvec_par = (int *) calloc(dim_par, sizeof(int));
11.6. ASIGNACIÓN DINÁMICA DE MEMORIA 127

57 if(pvec_par == NULL){ /* Estamos sin memoria */


58 printf("Error: no hay suficiente memoria para un vector de %d elementos\n",
59 dim_par);
60 free(pvec_usu); /* Antes de salir hay que LIBERAR la memoria que ya nos
61 habı́an dado */
62 exit(1);
63 }
64
65 /* Se copian los elementos pares */
66 m = 0; /* Índice para el vector de elementos pares (inicialmente apunta al
67 primer elemento) */
68 for(n=0; n<dim_usu; n++){
69 if( (pvec_usu[n]%2) == 0 ){ /* es par */
70 pvec_par[m] = pvec_usu[n]; /* copio el elemento */
71 m++; /* e incremento el ı́ndice del vector par */
72 }
73 }
74
75 /* Se imprimen el vector de elementos pares */
76
77 printf("\n-----------------------\n"); /* Para separar los dos vectores */
78 for(n=0; n<dim_par; n++){
79 printf("Elemento par %d = %d\n", n, pvec_par[n]);
80 }
81
82 /* y antes de salir se libera la memoria que nos habı́an asignado */
83
84 free(pvec_usu);
85 free(pvec_par);
86
87 exit(0);
88 }

Un ejemplo de la ejecución del programa se muestra a continuación:

Introduzca la dimensión del vector: 3


Elemento 0 = 1
Elemento 1 = 2
Elemento 2 = 4

-----------------------
Elemento par 0 = 2
Elemento par 1 = 4

Ejercicios
1. Modificar el programa anterior para que la petición de datos se realice en una función. El
prototipo de dicha función ha de ser: void PideVec(int *pvec, int dimension)

2. Puesto que los vectores siempre se recorren desde principio a fin y de elemento en
elemento puede ser más fácil usar punteros para recorrer los vectores. Modificar el pro-
grama del apartado anterior para que la búsqueda de elementos pares, la creación de
la matriz de números pares y su impresión, se realicen mediante punteros en lugar de
128 CAPÍTULO 11. PUNTEROS

mediante ı́ndices (para no perder los punteros al principio del bloque de forma que po-
damos realizar un free() sin problemas se recomienda definir dos punteros auxiliares
e inicializarlos con el mismo valor que los punteros base.)
Capı́tulo 12

Archivos

12.1. Introducción
Cualquiera que haya manejado un ordenador habrá experimentado con desolación como un
corte de corriente hace que desaparezca en un momento todo el trabajo que no habı́a guardado
en el disco. La razón de semejante desastre estriba en que la memoria RAM del ordenador
se borra en cuanto se corta la corriente. Además la memoria RAM es muy cara por lo que su
cantidad es limitada, pero sin embargo es necesario que dentro de un ordenador se almacenen
una gran cantidad de programas y de datos que pueden ocupar varios Gigabytes. Por todo esto
es necesario un sistema de almacenamiento secundario que no se borre al cortar la corriente
y que tenga una gran capacidad. El sistema actual de almacenamiento secundario más usado
es el disco duro, dada su rapidez y capacidad de almacenamiento, aunque existen una gran
variedad de sistemas como diskettes, CDROM. . .
El manejo de todos estos sistemas lo realiza el sistema operativo, el cual organiza la in-
formación en forma de archivos repartidos en directorios (también llamados carpetas). De
esta forma cada archivo tiene una identificación única dentro del ordenador compuesta por el
nombre del archivo y por la ruta o camino de acceso hasta él por el árbol de directorios.
Hasta ahora los programas que hemos realizado en este curso han tomado datos desde
el teclado y han impreso sus resultados en la pantalla. Sin embargo en muchas ocasiones es
necesario leer datos desde un archivo que se ha almacenado previamente en el disco (bien por
el propio usuario o bien por otro programa), escribir los resultados en un archivo de forma
que pueda ser usado posteriormente tanto por el usuario como por otro programa o usar un
archivo en disco como medio de almacenamiento de los datos de un programa de forma que
se mantengan cuando se apague el ordenador (por ejemplo en aplicaciones de bases de datos).
La biblioteca estándar de C proporciona un conjunto de funciones para el manejo de
archivos. En este tema introductorio sólo se van a estudiar cuatro funciones básicas que per-
miten trabajar con archivos de texto.

12.2. Apertura de archivos. La función fopen()


Antes de usar un archivo en disco es necesario decirle al sistema operativo que lo localice,
que evite que otros procesos accedan al archivo mientras nuestro programa lo tenga abierto y
que reserve unas zonas de memoria para trabajar con el archivo. Esto se realiza con la función
fopen() cuyo prototipo es:

129
130 CAPÍTULO 12. ARCHIVOS

Modo Descripción
r Abre el archivo para leer. El archivo ha de existir. Se posiciona al principio del
archivo.
r+ Abre el archivo para leer y escribir. El archivo ha de existir. Se posiciona al
principio del archivo.
w Abre el archivo para escribir. Si el archivo existe, borra su contenido y si no
existe, crea uno nuevo.
w+ Abre el archivo para escribir y leer. Si el archivo existe, borra su contenido y
si no existe, crea uno nuevo.
a Abre el archivo para añadir. Si el archivo no existe crea uno nuevo, pero si
existe no borra su contenido. Se posiciona al final del archivo de forma que
sucesivas escrituras se añaden al archivo original.
b Ha de añadirse a cualquiera de los modos anteriores si el archivo que se va a
abrir contiene datos binarios en lugar de texto ASCII1 .

Cuadro 12.1: Modos de apertura de los archivos.

FILE *fopen(char *Nombre completo del archivo, char *modo);

En donde Nombre completo del archivo es una cadena de caracteres que contiene el nom-
bre del archivo, incluyendo el camino completo si dicho archivo no esta situado en el directorio
actual. Este nombre ha de ser además un nombre legal para el sistema operativo. Por ejemplo
si se trabaja en MS-DOS el nombre de archivo no puede tener más de ocho caracteres seguidos
de un punto y tres caracteres más para la extensión.
El modo del archivo es otra cadena de caracteres que indica el tipo de operaciones que
vamos a realizar con él. En la tabla 12.1 se muestran todos los modos aceptados por la función.
El valor devuelto por la función es un “puntero a archivo”, que es un puntero que apunta
a una estructura de datos llamada FILE que está definida en stdio.h y que contiene toda la
información que necesitan el resto de las funciones que trabajan con archivos, como el modo
del archivo, los buffers del archivo, errores. . . Si ocurre algún tipo de error en la apertura (como
intentar abrir un archivo que no existe en modo "r") fopen() devuelve un NULL. Por tanto
después de la llamada a fopen() hay que verificar que el puntero devuelto es válido (al igual
que se realiza con los punteros devueltos por calloc() y malloc()).
Por ejemplo si queremos abrir un archivo para leer de él cuyo nombre es “pepe” habrı́a
que escribir:

#include <stdio.h>
...
main(void)
{
FILE *pfich;
...
pfich = fopen("pepe", "r+");
if(pfich == NULL){
printf("Error: No se puede abrir el archivo.\n");
exit(1);
}
...
1 En este curso no se va a trabajar con ficheros binarios debido a su mayor complejidad.
12.3. CIERRE DE ARCHIVOS. LA FUNCIÓN FCLOSE(). 131

en donde se ha supuesto que el archivo “pepe” está situado en el directorio de trabajo actual. En
caso de que no exista el archivo “pepe” o si no se encuentra en el directorio actual, fopen() no
podrá abrirlo y devolverá un NULL, por lo que el programa después de imprimir un mensaje
de error terminará su ejecución.
Si se quiere abrir un archivo para añadir al final de él más datos y dicho archivo está situado
en el directorio c:\dani con el nombre datos.m, el programa serı́a igual que el anterior salvo
que la llamada a fopen() serı́a:
pfich = fopen("c:\\dani\\datos.m", "a");
Recuérdese que \\ es traducido por el compilador de C por una \ al generar la cadena
de caracteres, de la misma manera que \n se traduce por un salto de carro. El archivo abierto
será por tanto c:\dani\datos.m.

12.3. Cierre de archivos. La función fclose().


Una vez que se ha terminado de usar un archivo hay que cerrarlo. Con la operación de
cierre se escriben a disco todos los datos que aun queden en el buffer 2 , se “desconecta” el
archivo del programa y se libera el puntero al archivo. Como la mayorı́a de los sistemas oper-
ativos tienen limitado el máximo número de archivos que un programa puede tener abiertos a
la vez, es conveniente cerrar los archivos una vez que ya no se necesite usarlos. Sin embargo
hay que ser cuidadoso para no seguir usando el puntero al archivo que se acaba de cerrar, pues
los resultados serán impredecibles. Por si esto fuera poco, también es muy peligroso cerrar un
archivo más de una vez.
Para evitar pérdidas de datos, cuando un programa termina normalmente 3 la función fclose()
se llama automáticamente para cerrar todos los archivos abiertos. A pesar de esto, es conve-
niente acostumbrarse a cerrar uno mismo los archivos que haya abierto por si acaso falla el
cierre automático (pues ello ocasionarı́a pérdidas en nuestros datos) y ası́ se exigirá en este
curso.
El prototipo de la función fclose() es:

int fclose(FILE *puntero al archivo);

En donde puntero al archivo es el puntero a archivo devuelto por la función fopen() al


abrir el archivo que se desea cerrar ahora.
El valor devuelto por la función es cero si el archivo se cerró con éxito o -1 si ocur-
rió algún tipo de error al cerrarlo; aunque independientemente de este resultado el archivo
se cierra igualmente. Errores tı́picos pueden ser que el puntero que le pasamos no apunta a
ningún archivo o que el disco se ha llenado y no se ha podido vaciar el buffer con la consigu-
iente pérdida de datos. Por esto último es conveniente verificar que el archivo se ha cerrado
correctamente comprobando el valor de retorno, pues aunque no podemos hacer ya nada para
solucionar la pérdida de datos, por lo menos podemos avisar al usuario de que ha ocurrido
algún problema.
Para terminar veamos por ejemplo cómo se harı́a para cerrar el archivo que se abrió en el
ejemplo de la sección 12.2:
2 Para lograr una mayor eficiencia en los accesos a disco, las escrituras del programa se realizan en una memoria

intermedia, llamada buffer, vaciándose ésta al disco cuando existe una cantidad suficiente de bytes o al cerrar el
archivo como se acaba de decir.
3
Aquı́ la palabra normalmente incluye cualquier salida controlada del programa, es decir, mediante una llamada
a exit() o por la llegada al final del main(). Un programa que se aborta con Ctrl-C o que termina inesperadamente
por un error de programación (división por cero. . . ) no se considera como terminado normalmente.
132 CAPÍTULO 12. ARCHIVOS

if( fclose(pfich) != 0){


printf("Error al cerrar el archivo\n");
printf("Es posible que se haya producido una pérdida de datos.\n");
printf("Lo siento.\n");
}

12.4. Lectura y escritura en archivos. Las funciones fprintf()


y fscanf().
Para escribir y leer de archivos de texto se usan las funciones fprintf() y fscanf(),
cuyo funcionamiento es idéntico al de printf() y scanf() respectivamente, salvo que ahora
es necesario indicar en qué archivo han de escribir o leer.

12.4.1. La función fprintf()


El prototipo de la función fprintf() es:

fprintf(FILE *puntero al archivo, const char *cadena de formato, ...);

En donde puntero al archivo es el puntero a archivo devuelto por la llamada a fopen()


al abrir el archivo sobre el que se desea escribir o leer. La cadena de formato es una cadena
que especifica el formato en el que se imprimen el resto de argumentos de la llamada. Esta
cadena se construye de la misma forma que la de la función printf(), la cual se describió en
el la sección 3.7. Por último los tres puntos (...) del prototipo de printf() indican que a
la cadena de formato le sigue un número variable de parámetros, los cuales se imprimirán
según lo especificado en ella.
Por ejemplo, para escribir en el archivo abierto en la sección 12.2 no hay más que hacer:

fprintf(pfich, "El valor de %d en hexadecimal es %x\n", 15, 15);

12.4.2. La función fscanf()


la función fscanf() tiene por prototipo:

int fscanf(FILE *puntero al archivo, const char *cadena de formato, ...);

El valor devuelto por fscanf() es un poco complejo de describir. En caso de que solo
se quiera leer un valor (por ejemplo fscanf(pfich, "%d", &num)) el valor devuelto es 1
si se ha leı́do correctamente el valor o 0 si ha ocurrido un error en la lectura por una mala
especificación del formato (por ejemplo si en este caso que se esperaba un entero (%d) en el
archivo se encuentra una cadena de caracteres). Si no se ha leı́do nada porque se ha llegado al
final del archivo, el valor devuelto será EOF.
En el caso de que se quieran leer varios valores en una misma llamada a fscanf() (Por
ejemplo fscanf(pfich, "%d %d", &n1, &n2)), fscanf() devuelve el número de valores
que se han leı́do (y que por tanto estarán ya escritos en sus respectivas variables 4). Si ocurre un
error antes de haber realizado alguna lectura, el comportamiento es igual al descrito anterior-
mente para el caso en el que sólo se leı́a un argumento (0 si hay error de formato o EOF si se ha
4 Recuérdese que fscanf() escribe directamente en la posición de memoria de la variable en la que se desea

guardar el valor leı́do.


12.4. LECTURA Y ESCRITURA EN ARCHIVOS. LAS FUNCIONES FPRINTF() Y FSCANF().133

llegado al final del archivo). Si en cambio el error ocurre cuando ya se ha realizado alguna lec-
tura, pero no todas, el valor devuelto es igual al número de valores leı́dos, independientemente
de que el error producido haya sido de formato o de fin de archivo.
Para ilustrar el uso de fscanf() supongamos que en el archivo abierto en la sección 12.2
hay escritos una serie de números en punto flotante de los cuales se desea calcular su suma.
En el siguiente listado se muestra un programa que abre el archivo, lee los datos y calcula la
suma:

1 /* Programa: Suma
2 *
3 * Descripción: Lee el archivo "pepe" que contiene números en punto flotante
4 * y calcula su suma.
5 *
6 * Revisión 0.0: 25/05/1998
7 *
8 * Autor: José Daniel Muñoz Frı́as.
9 */
10
11 #include <stdio.h>
12 #include <stdio.h>
13
14 void main(void)
15 {
16 int n; /* Valor devuelto por scanf */
17 FILE *pfich; /* Puntero al archivo */
18 double dato; /* dato leı́do del fichero */
19 double sum_tot; /* Suma de los datos */
20
21 pfich = fopen("pepe", "r+");
22 if(pfich == NULL){
23 printf("Error: No se puede abrir el fichero \"pepe\"\n");
24 exit(1);
25 }
26
27 sum_tot = 0; /* Inicialización de la suma antes de entrar en el bucle*/
28 n = fscanf(pfich, "%lf", &dato);
29 while(n!=EOF && n!=0){ /* termina en cuanto ocurra un error de lectura*/
30 sum_tot+=dato;
31 n = fscanf(pfich, "%lf", &dato);
32 }
33
34 printf("El valor de la suma es: %lf\n", sum_tot);
35
36 if( fclose(pfich) != 0){
37 printf("Error al cerrar el fichero\n");
38 exit(1);
39 }
40 }

Este programa, si el archivo contiene:


1.3
3.4
4.5
134 CAPÍTULO 12. ARCHIVOS

123.4
al ejecutarse imprimirá por pantalla:
El valor de la suma es: 132.600000
Como se puede apreciar en las lı́neas 27 y 30, el uso de fscanf() es idéntico al uso de
scanf() salvo por la existencia del puntero al archivo desde donde se lee (pfich).
También cabe destacar la condición de salida del bucle while. Como se puede apreciar en
la lı́nea 28 se sale cuando el valor devuelto por fscanf() sea EOF, en cuyo caso se habrá acaba-
do el fichero y no tiene sentido seguir leyendo; o cuando devuelva un cero, indicando que no
se ha leı́do el dato debido a un error de formato. Según esto último, si el archivo “pepe” tuviese
el siguiente contenido:

2.3
3
hola tı́o
4
5.89

¿cual serı́a la salida del programa?

12.5. Ejemplo
Para fijar ideas vamos a mostrar a continuación un ejemplo completo en el que se traba-
ja con archivos. En los primeros tiempos de la informática sólo existı́an terminales en modo
texto, por lo que cuando algún programa tenı́a que representar gráficamente una función se
recurrı́a frecuentemente a usar la impresora como salida gráfica. Vamos a retroceder por tanto
unos cuantos años en la historia de la informática y vamos a realizar un programa que, par-
tiendo de un archivo en el que están escritas las notas de los alumnos de programación, genere
otro archivo con un histograma de las notas. En dicho histograma una barra representará el
número de ocurrencias en el archivo de la nota correspondiente. Por ejemplo si el archivo de
notas es el siguiente:
7.1
7.2
9.2
6.5
8.3
8.4
7.5
6.7
5.6
9.4
10
9
8
5.5
5.6
5.0
6.7
12.5. EJEMPLO 135

6.4
6.2
6.0
6.1
7.0
7.9
7.6
7.7
8.5
10

lo cual indica que todos los alumnos han estudiado un montón, el programa generarı́a el sigu-
iente archivo:

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

La forma de interpretar esta gráfica es la siguiente: como en la lı́nea del 5 hay cuatro
asteriscos, esto indica que hay cuatro notas entre 5 y 6 (sin incluir el 6) o dicho de una manera

más formal, el número de notas contenidas en el intervalo 5 6 es cuatro. La excepción es el
10; su lı́nea indica el número de notas iguales a 10.
El programa a realizar se puede dividir en varias tareas:

Abrir los archivos.

Leer el archivo de notas y contar cuantas notas están dentro de un intervalo dado. Para
ello se usará un vector de enteros en el que en su primer elemento almacenaremos el

número de notas contenidas en el intervalo 0 1 , en el segundo las contenidas en 1 2 
y ası́ sucesivamente. La excepción será el elemento número 10 que sólo almacenará las
notas iguales a 10.

Escribir en un archivo el histograma. Para ello no hay más que imprimir una lı́nea para
cada intervalo, en la que se escribirán tantos asteriscos como notas haya en dicho inter-
valo (lo cual está indicado en el número almacenado en el elemento del vector corre-
spondiente a dicho intervalo).

La primera tarea se ha realizado dentro de la función main(), pero las otras dos se han
realizado en las funciones CuentaNotas() y EscribeHisto() respectivamente. El listado
del programa completo se muestra a continuación:

1 /* Programa: ImpGraf
2 *
3 * Descripción: Lee un archivo que contiene las notas de los alumnos (una
136 CAPÍTULO 12. ARCHIVOS

4 * nota (con decimales) por lı́nea almacenada) y genera otro


5 * archivo con un histograma de las notas.
6 *
7 * Revisión 0.0: 25/05/1998
8 *
9 * Autor: José Daniel Muñoz Frı́as.
10 */
11
12 #include <stdio.h>
13
14 void CuentaNotas(FILE *pfnot, int histo[]);
15 void CreaHisto(FILE *pfhis, int histo[]);
16
17 void main(void)
18 {
19 int histo_not[11]; /* Vector para almacenar el número de ocurrencias
20 de la nota correspondiente. Si histo_not[9] vale
21 50 es que 50 alumnos han sacado una nota entre 9
22 y 10 (ánimo chicos/as) */
23
24 FILE *pfnotas; /* Puntero al archivo de notas */
25 FILE *pfhisto; /* Puntero al archivo de histograma */
26
27 /* Se abren los dos ficheros */
28 pfnotas = fopen("notas","r");
29 if(pfnotas == NULL){
30 printf("Error: No se ha podido abrir el fichero \"notas\"\n");
31 exit(1);
32 }
33
34 pfhisto = fopen("histogra","w");
35 if(pfhisto == NULL){
36 printf("Error: No se ha podido abrir el fichero \"histogra\"\n");
37 fclose(pfnotas); /* cierro el fichero que ya habı́a abierto */
38 exit(1);
39 }
40
41 CuentaNotas(pfnotas, histo_not);
42
43 CreaHisto(pfhisto, histo_not);
44
45 fclose(pfnotas);
46 fclose(pfhisto);
47 }
48
49 /* Función: CuentaNotas()
50 *
51 * Descripción: Lee las notas del fichero cuyo puntero se pasa como argumento
52 * y crea el vector con el histograma.
53 *
54 * Argumentos: FILE *pfnot: Puntero al archivo de notas.
55 * int histo[]: Histograma.
56 *
57 * Revisión 0.0: 25/05/1998.
12.5. EJEMPLO 137

58 *
59 * Autor: José Daniel Muñoz Frı́as.
60 */
61
62 void CuentaNotas(FILE *pfnot, int histo[])
63 {
64 int n; /* Número de datos leı́do por fscanf() */
65 int i; /* Índice para los for */
66 double nota_act; /* Almacena la nota que se acaba de leer del fichero */
67
68 /* Inicializo la matriz del histograma */
69 for(i=0; i<11; i++){
70 histo[i] = 0;
71 }
72
73 n = fscanf(pfnot, "%lf", &nota_act);
74 while(n!=EOF && n!=0){ /* termina en cuanto ocurra un error de lectura */
75 if( (nota_act < 0) || (nota_act > 10) ){ /* Se verifica que las notas
76 sean válidas */
77 printf("Error en el fichero de entrada. Las notas han de estar entre"
78 "0 y 10\n");
79 exit(1);
80 }
81 for(i=0; i<11; i++){ /* Se busca en qué casilla está la nota */
82 if( (i <= nota_act) && (nota_act < i+1) ){
83 histo[i]++;
84 break; /* una vez que he situado la nota en su casilla salgo del for */
85 }
86 }
87 n = fscanf(pfnot, "%lf", &nota_act);
88 }
89 }
90
91 /* Función: CreaHisto()
92 *
93 * Descripción: Representa "gráficamente" en un fichero el histograma que
94 * se pasa en el vector histo. Para ello por cada elemento de
95 * dicho vector se transforma en una fila de asteriscos (un
96 * asterisco representa una unidad)
97 *
98 * Argumentos: FILE *pfhis: Puntero al archivo de notas.
99 * int histo[]: Histograma.
100 *
101 * Revisión 0.0: 25/05/1998.
102 *
103 * Autor: José Daniel Muñoz Frı́as.
104 */
105
106 void CreaHisto(FILE *pfhis, int histo[])
107 {
108 int i; /* Índice para los for */
109 int contador; /* cuenta los asteriscos escritos */
110
111 for(i=0; i<11; i++){
138 CAPÍTULO 12. ARCHIVOS

112 contador = 0;
113 fprintf(pfhis,"%2d ", i); /* Escribe la nota a la que corresponde la
114 "barra" del histograma */
115 while(histo[i] > contador){
116 fprintf(pfhis, "*");
117 contador++;
118 }
119 fprintf(pfhis, "\n"); /* Escribe el salto de lı́nea para que la
120 siguiente nota se escriba en otra lı́nea */
121 }
122 }

Ejercicios
1. Modificar el programa anterior para que sea fácil cambiar el carácter con el que se im-
prime el histograma. Para ello usar la sentencia #define (por ejemplo #define CAR_HISTO ’#’).
2. Realizar lo mismo que en el ejercicio anterior pero pidiéndole al usuario al principio del
programa el carácter con el que se desea que se imprima el histograma.
3. Realizar un programa que a partir de un archivo que contiene una serie de números (en
el mismo formato que el usado para el archivo de notas) calcule la media y la varianza
de la serie y la imprima por pantalla. Para ello se aconseja usar funciones que lean el
archivo y que devuelvan el resultado de sus cálculos. Ası́ por ejemplo para calcular la
media se podrı́a crear una función con el prototipo double media(FILE *pfich);
4. Realizar un programa que a partir del archivo de notas cree un archivo con el histograma
de notas y a continuación imprima la media y la varianza de las notas. Nota: Para volver
a leer un archivo desde el principio se puede usar la función rewind(), cuyo prototipo es: void
rewind(FILE *pfich);

5. Realizar un histograma en el que los intervalos representados sean de medio punto,



es decir, se tendrá una fila para las notas contenidas en 0 0 5 , otra para 0 5 1 y 
ası́ sucesivamente. Para ello usar una sentencia #define para crear un parámetro que
indique el número de intervalos en que se divide el conjunto de notas.

12.6. Funciones de entrada y salida a archivo sin formato


Hasta ahora toda la entrada y salida a archivos se ha realizado exclusivamente con las
funciones fprintf() y fscanf(). Estas funciones tienen la ventaja de realizar las comple-
jas conversiones de formato que son necesarias para poder transformar los valores binarios
usados internamente en el ordenador a cadenas de caracteres usadas por los humanos. Por
ejemplo antes de imprimir un número entero, que conviene recordar que se almacena como
una secuencia de bits en la memoria del ordenador, es necesario transformar esa secuencia de
bits en una secuencia de caracteres que represente al número en la base elegida (normalmente
base 10 (%d) o base 16 (%x)). El inconveniente de esto es que el código encargado de realizar
las conversiones ocupa memoria y tarda algo de tiempo en ejecutarse. Si embargo en la may-
orı́a de los casos no es necesario escribir o leer números. Por ello en la librerı́a estándar de C
existen funciones que que escriben o leen caracteres (o cadenas de caracteres) sin conversión
de formatos. Estas funciones son fgetc()/fputc() para leer/escribir un carácter de/en un
archivo y fgets()/fputs() para leer/escribir una cadena de caracteres de/en un archivo. Los
prototipos de estas funciones son:
12.6. FUNCIONES DE ENTRADA Y SALIDA A ARCHIVO SIN FORMATO 139

int fgetc(FILE *puntero a archivo);


int fputc(int carácter, FILE *puntero a archivo);
char *fgets(char *cadena, int tam cad, FILE *puntero a archivo);
int fputs(const char *cadena, FILE *puntero a archivo);

La función fgetc() lee el siguiente carácter desde el archivo como un unsigned char
y lo devuelve convertido a int. Si se llega al final del fichero u ocurre algún error el valor
devuelto es EOF5
La función fputc() escribe el carácter (convertido a unsigned char) que se le pasa
como argumento en el archivo al que apunta puntero a archivo. El valor devuelto es el carácter
escrito (convertido a int) o EOF si ha ocurrido algún error.
fgets() lee una cadena de caracteres del archivo puntero a archivo y los almacena en
la cadena de caracteres cadena. La lectura se acaba cuando se encuentra el carácter de nueva
lı́nea ’\n’ (que se escribe en la cadena), cuando se encuentra el fin de fichero (en este caso
no se escribe ’\n’ en la cadena) o cuando se han leı́do tam cad 1 caracteres. En todos estos
casos se escribe un carácter ’\0’ en la cadena a continuación del último carácter leı́do. Por
supuesto la cadena de caracteres ha de tener espacio suficiente como para almacenar todos los
caracteres que se puedan leer en la función (tam cad). El valor devuelto es un puntero a la
cadena leı́da o NULL si ha ocurrido algún error o se ha llegado al final del fichero.
fputs() escribe la cadena en el archivo al que apunta puntero a archivo sin el ’\0’ del
final. El valor devuelto es un número positivo si se ha escrito la cadena correctamente o EOF
en caso de error.
Para ilustrar el uso de estas funciones vamos a estudiar un ejemplo:

Imprimir un archivo
Vamos escribir en C un programa que imprima por pantalla el contenido de un fichero
cuyo nombre se pide al usuario al principio del programa (el funcionamiento del programa
será similar al del comando type de MS-DOS). El programa, después de abrir el archivo,
leerá carácter a carácter el contenido del archivo e irá imprimiendo dichos caracteres por
pantalla. Un pseudocódigo del programa es el siguiente

Pedir al usuario el nombre del archivo que se ha de imprimir.


Mientras no se llegue al final del archivo{
leer un carácter del archivo
imprimir el carácter
}
Cerrar el archivo.

Y un programa que realiza esta tarea se muestra a continuación.

1 /* Programa: ImpArch
2 *
3 * Descripción: Imprime el contenido del archivo cuyo nombre se pide al
4 * usuario por la pantalla.
5 Puede parecer un poco extraño el leer un carácter y devolverlo como un int. La razón de esto es la de poder

distinguir los caracteres normales de los errores, pues si se devolviese el carácter leı́do como un unsigned char, al
ocupar la tabla ASCII todos los valores que se pueden representar mediante un unsigned char, no quedan valores
libres para representar el EOF. La solución es devolver un entero en el que los caracteres ocupan de 0 a 255 de forma
que el resto de valores puedan usarse para devolver códigos de error.
140 CAPÍTULO 12. ARCHIVOS

5 *
6 * Revisión 0.0: 26/05/1998
7 *
8 * Autor: José Daniel Muñoz Frı́as.
9 */
10
11 #include <stdio.h>
12
13 #define TAM_CAD 256
14
15 main(void)
16 {
17 char nom_fich[TAM_CAD]; /* Nombre del fichero que se va a abrir */
18 char letra; /* carácter leı́do desde el fichero y escrito en
19 pantalla */
20 FILE *pfich; /* Puntero al fichero que se imprime */
21
22 printf("Introduzca el nombre del fichero que desea imprimir: ");
23 gets(nom_fich);
24
25 pfich = fopen(nom_fich, "r");
26 if(pfich == NULL){
27 printf("Error al intentar abrir el fichero \"%s\"\n", nom_fich);
28 exit(1);
29 }
30
31 letra = fgetc(pfich);
32 while(letra != EOF){
33 printf("%c", letra);
34 letra = fgetc(pfich);
35 }
36
37 fclose(pfich);
38 }
Capı́tulo 13

Estructuras de datos

13.1. Introducción
En muchas situaciones un objeto no puede describirse por un solo dato, sino por una serie
de ellos. Por ejemplo el historial médico de un paciente consta de varios campos, siendo cada
uno de ellos es de un tipo distinto. Un ejemplo de historial médico, junto con los tipos de datos
utilizados podrı́a ser el mostrado en la siguiente tabla:

Dato Tipo de dato Tipo en C


Nombre Cadena de caracteres char[100]
Edad Número entero unsigned short
Peso Número en punto flotante float
No Seguridad Social Número entero unsigned long
Historia Cadena de caracteres char[1000]

Si se desea realizar un programa para gestionar una base de datos con los historiales de
los pacientes, serı́a mucho más cómodo poder agrupar todos los datos de un paciente bajo una
misma variable compuesta. En C estos tipos de variables definidos como una agrupación de
datos, que pueden ser de distintos tipos, se denominan estructuras.
Hasta ahora habı́amos visto que C disponı́a de unos tipos básicos para manejar números y
caracteres y que a partir de estos tipos básicos se podı́an crear vectores y matrices, que eran
agrupaciones de datos pero de un mismo tipo. Las estructuras de datos son un paso más allá, al
permitir agrupar en una misma variable datos no solo de un mismo tipo, sino de tipos variados,
tal como se va a mostrar en este capı́tulo.

13.2. Declaración y definición de estructuras


La sintaxis de la declaración de una estructura es:

struct nombre de estructura{


tipo 1 nombre miembro 1;
tipo 2 nombre miembro 2;
...
tipo n nombre miembro n;

141
142 CAPÍTULO 13. ESTRUCTURAS DE DATOS

};

Esta declaración no reserva espacio en la memoria para la estructura; simplemente crea


una plantilla con el formato de la estructura. La sintaxis para definir una variable del tipo
estructura previamente declarado es:

struct nombre de estructura variable;

Esta sentencia reservará un espacio en la memoria para albergar una estructura nom-
bre de estructura, a la cual se podrá acceder mediante la etiqueta variable.
Por ejemplo la declaración de una estructura para almacenar el historial médico de un
paciente se escribirı́a:

struct paciente{
char nombre[100];
unsigned short edad;
float peso;
unsigned long n_seg_social;
char historia[1000];
};

Y para crear una variable del tipo estructura paciente se escribe:

struct paciente pepito;

Nótese que una vez declarada una estructura, dicha estructura se puede considerar como
un nuevo tipo de dato con el nombre struct nombre de estructura, y que la declaración de
variables de ese nuevo tipo de dato es idéntica a la del resto de variables; compárense si no las
dos definiciones siguientes:

unsigned long numero;


struct paciente juanito;

13.3. La sentencia typedef


typedef permite dar un nombre arbitrario a un tipo de datos de C, ya sea un tipo básico
(int, char. . . ) o derivado (struct paciente. . . ).
Por ejemplo si se sabe que en un PC un short ocupa dos bytes, se puede escribir al
comienzo del programa la definición de tipos:

typedef unsigned short WORD;

Y a partir de entonces, WORD se convierte en un sinónimo de unsigned short, por lo que


si se desea definir una variable de dos bytes se puede hacer:

WORD edad;

En lugar de:

unsigned short edad;


13.4. ACCESO A LOS MIEMBROS DE UNA ESTRUCTURA 143

Lo cual aparte del ahorro de espacio en la definición de enteros de dos bytes tiene la ventaja
de que si se desea portar el programa a una máquina en la que los enteros de dos bytes son el
tipo int sólo habrı́a que cambiar la declaración de WORD al principio del programa, dejando
intacto el resto de definiciones.
También se puede usar typedef para simplificar la definición de variables de tipo estruc-
tura. Para ello se puede unir la declaración de la estructura con una definición de tipos de la
forma:

typedef struct paciente{


char nombre[100];
unsigned short edad;
float peso;
unsigned long n_seg_social;
char historia[1000];
}PACIENTE;

La definición de variables de tipo estructura paciente se podrá realizar ahora como:

PACIENTE pepito;

Lo cual mejora bastante la legibilidad del programa.


En resumen el uso de typedef tiene dos finalidades:
Permite una mejor documentación del programa a dar nombres más significativos a los
tipos, especialmente a los derivados.
Mejora la portabilidad del programa, al poder encapsular tipos que dependan de la
máquina en sentencias typedef.

13.4. Acceso a los miembros de una estructura


Para acceder a los miembros de una estructura se usa el operador punto. Siguiendo con el
ejemplo del historial médico, para inicializar el historial pepito se escribirı́a:

strcpy(pepito.nombre, "José Pérez López");


pepito.edad = 27;
pepito.peso = 67.5;
pepito.n_seg_social = 27345190;
strcpy(pepito.historia, "Alérgico a las Sulfamidas");

y para imprimir los datos de un historial se podrı́a escribir:

printf("Paciente %s:\n Edad: %d\n Peso: %f\n N Seg Soc. %ld\n"


" Historia: %s\n", pepito.nombre, pepito.edad, pepito.peso,
pepito.n_seg_social, pepito.historia);

Como puede apreciarse, aunque pepito es una variable de tipo struct paciente, o lo
que es lo mismo, del tipo PACIENTE si se ha realizado el typedef de la sección 13.3; sus
miembros obtenidos con el operador punto son variables normales, es decir, pepito.nombre
es una cadena de caracteres y como tal se inicializa con la función strcpy() y se imprime
con el formato %s. De la misma manera pepito.edad es un int y pepito.peso es un float.
En definitiva el uso de los miembros de una estructura es idéntico al de las demás variables.
144 CAPÍTULO 13. ESTRUCTURAS DE DATOS

13.5. Ejemplo: Suma de números complejos


Se desea realizar un programa que pida al usuario dos números complejos y muestre el
resultado de su suma. Para facilitar las cosas se va a declarar una estructura para almacenar un
número complejo, de forma que el programa sea más fácil de realizar y de entender.
A continuación se muestra un listado del programa:

1 /* Programa: SumaComp
2 *
3 * Descripción: Pide al usuario dos números complejos y calcula su suma
4 *
5 * Revisión 0.0: 10/05/1999
6 *
7 * Autor: El programador estructurado.
8 */
9
10 #include <stdio.h>
11
12 /* Declaración de una estructura para contener números complejos */
13 typedef struct complejo{
14 double real;
15 double imag;
16 }COMPLEJO;
17
18 void main(void)
19 {
20 COMPLEJO comp1, comp2; /* Los dos números complejos que se pedirán al
21 usuario desde el teclado */
22 COMPLEJO result; /* Resultado de comp1+comp2 */
23
24 printf("Introduzca la parte real del primer complejo: ");
25 scanf("%lf", &comp1.real);
26 printf("Introduzca la parte imaginaria del primer complejo: ");
27 scanf("%lf", &comp1.imag);
28
29 printf("Introduzca la parte real del segundo complejo: ");
30 scanf("%lf", &comp2.real);
31 printf("Introduzca la parte imaginaria del segundo complejo: ");
32 scanf("%lf", &comp2.imag);
33
34 result.real = comp1.real + comp2.real;
35 result.imag = comp1.imag + comp2.imag;
36
37 printf("\nEl resultado de la suma es %lf + %lf j\n", result.real,
38 result.imag);
39 }

Como puede apreciarse, en primer lugar se ha declarado la estructura complejo y se le


ha asociado el tipo COMPLEJO1 (lı́neas 13–16). Nótese además que dicha declaración se ha
realizado antes de comenzar la función main(). Esto último permite que el formato de la
estructura se pueda usar en todas las funciones del programa. Si por el contrario la declaración
1 El nombre de la estructura y el nombre del tipo no tienen por qué coincidir, aunque es conveniente para evitar

confusiones.
13.6. ESTRUCTURAS Y FUNCIONES 145

de la estructura complejo se hubiese escrito dentro de la función main(), dicha declaración


serı́a privada a la función main() y por tanto no se podrı́a usar fuera de dicha función.
En segundo lugar es necesario destacar que el uso de los miembros de una estructura es
idéntico al de una variable que tenga su tipo. Por ejemplo en la lı́nea 25 se usa el operador &
para obtener la dirección del miembro real de la estructura comp1 para que scanf() pueda
escribir en dicha variable el número que introduzca el usuario por el teclado. Conviene destacar
también que la precedencia del operador punto es mayor2 que la del operador & y por tanto la
dirección que se obtiene es la del miembro de la estructura.

13.6. Estructuras y funciones


Dos estructuras del mismo tipo se pueden igualar entre sı́, es decir si se desea por ejemplo
obtener una copia de una estructura del tipo paciente declarada en la sección 13.2 se puede
hacer simplemente:

...
struct paciente pepito;
struct paciente copia_de_pepito;
...
/* aquı́ estarı́a la inicialización de pepito */
...
copia_de_pepito = pepito;

Como se recordará en las llamadas a funciones los argumentos de la función se copian en


unas variables locales y el valor devuelto por la función también se copia a la variable a la
que se asigna la función en el programa principal. Por tanto una función para inicializar una
estructura del tipo struct complejo se puede escribir:

struct complejo InicComplejo(double real, double imag)


{
struct complejo resultado;

resultado.real = real;
resultado.imag = imag;

return resultado;
}

Para llamar a la función desde un programa se escribirı́a simplemente:

...
struct complejo comp1;
...
comp1 = InicComplejo(2.0, 3.7);
...

Como se puede apreciar la función recibe dos números reales y crea un número complejo
llamado resultado. Dicha estructura al terminar la función es devuelta al programa principal,
en donde se copia a la estructura comp1, que es también una estructura de tipo complejo.
2 De hecho el operador punto, junto con el operador corchete ([ ]) para referenciar elementos de matrices y el

operador flecha (->) que se estudiará mas adelante, son los operadores con la mayor precedencia en C.
146 CAPÍTULO 13. ESTRUCTURAS DE DATOS

Ejercicios
1. Suponiendo que se ha definido con typedef la estructura struct complejo como
COMPLEJO, tal como se hizo en el ejemplo de la sección 13.5; modificar los ejemplos
anteriores para usar el nuevo tipo COMPLEJO.
El paso de parámetros es similar. Por ejemplo una función que devuelva la suma de dos
complejos que se pasan como argumentos serı́a:
COMPLEJO SumaComp(COMPLEJO c1, COMPLEJO c2)
{
COMPLEJO res;

res.real = c1.real + c2.real;


res.imag = c1.imag + c2.imag;

return res;
}

Ejercicios
1. Modificar el programa de la sección 13.5 para que la suma de los dos números complejos
se realice mediante una llamada a la función SumaComp().
Sin embargo el paso de estructuras de un tamaño elevado a una función es un proceso
costoso, tanto en memoria como en tiempo. Por ello el paso de estructuras a funciones sólo
debe de realizarse cuando estas sean de pequeño tamaño, tal como se acaba de hacer con la
estructura COMPLEJO. Por ejemplo si se quiere pasar a una función una estructura del tipo
PACIENTE, descrita en la sección 13.3, se copiarı́an 1110 bytes. En estos casos se opta siempre
por pasar a la función un puntero a la estructura en lugar de la estructura completa, del mismo
modo que con los vectores y matrices.

13.7. Punteros a estructuras


Un puntero a estructura se declara de la misma forma que un puntero a una variable ordi-
naria. Ası́ por ejemplo en la declaración:
struct paciente *ppaciente;
Se ha creado un puntero a una estructura de tipo paciente, declarada en la sección 13.2. Si
se usa typedef para definir un “alias” a la declaración de estructura, tal como se hizo en la
sección 13.3, se podrı́a crear el mismo puntero de la forma:
PACIENTE *ppaciente;
Para obtener la dirección de una estructura se usa el operador & al igual que con el resto
de variables. Por ejemplo, para hacer que el puntero ppaciente apunte a la estructura pepito
se escribirı́a:
PACIENTE pepito; /* Historial de un paciente */
PACIENTE *ppaciente; /* Puntero a un historial de un paciente */

ppaciente = &pepito; /* ppaciente ahora apunta a pepito */


13.7. PUNTEROS A ESTRUCTURAS 147

Para acceder a un elemento de una estructura a través de un puntero se utiliza el operador


de indirección *, al igual que con el resto de variables, ası́ si ppaciente apunta a una es-
tructura de tipo paciente, *ppaciente es la estructura y (*ppaciente).edad es la edad del
paciente. Si en el ejemplo anterior se desea imprimir la ficha del paciente usando el puntero
ppaciente hay que escribir:

printf("Paciente %s:\n Edad: %d\n Peso: %f\n N Seg Soc. %ld\n"


" Historia: %s\n", (*ppaciente).nombre, (*ppaciente).edad,
(*ppaciente).peso, (*ppaciente).n_seg_social,
(*ppaciente).historia);

En estos casos los paréntesis son estrictamente necesarios, pues tal como se dijo antes, el
operador punto tiene una precedencia mayor que el operador *. Si no se ponen los paréntesis
*ppaciente.edad accederı́a a la dirección de memoria contenida en edad, es decir, serı́a
equivalente a *(ppaciente.edad). Como edad no es un puntero sino un entero, el pro-
grama accederá a una posición de memoria indeterminada y que además no le pertenecerá,
provocándose un grave error.
Como los punteros a estructuras se usan con bastante frecuencia en C, existe una notación
alternativa para acceder a un miembro de una estructura mediante un puntero, que es el oper-
ador “flecha” ->3 . Ası́ por ejemplo, las dos asignaciones siguientes son equivalentes:

(*ppaciente).edad = 32;
ppaciente->edad = 32;

13.7.1. Paso de punteros a estructuras a funciones


Tal como se dijo en la sección 13.6 el paso de estructuras de gran tamaño a funciones
mediante copia es tremendamente ineficiente. En estos casos se puede pasar a la función la
dirección de la estructura original en lugar de una copia de ésta. Ésto tiene la ventaja de
aumentar considerablemente la rapidez del proceso de llamada de la función al tener que
copiarse sólo un puntero a la estructura en lugar de la estructura completa. Sin embargo, dado
que la función trabaja ahora con la estructura original en lugar de con una copia de ésta, se
han de extremar las precauciones para no modificar accidentalmente el contenido de ésta.
Por ejemplo, una función para imprimir una estructura del tipo PACIENTE serı́a:

void ImpriPaciente(PACIENTE *ppaciente)


{
printf("Paciente %s:\n Edad: %d\n Peso: %f\n N Seg Soc. %ld\n"
" Historia: %s\n", ppaciente->nombre, ppaciente->edad,
ppaciente->peso, ppaciente->n_seg_social,
ppaciente->historia);
}

Y una función para inicializar una estructura del tipo PACIENTE pidiéndole los datos al
usuario serı́a:

void InitPaciente(PACIENTE *ppaciente)


{
printf("Nombre del paciente: ");
gets(ppaciente->nombre);
3 El operador flecha está formado por un - (sı́mbolo menos) y un > (sı́mbolo mayor que)
148 CAPÍTULO 13. ESTRUCTURAS DE DATOS

printf("Edad del paciente: ");


scanf("%d", &ppaciente->edad );

printf("Peso del paciente: ");


scanf("%f", &ppaciente->peso );

printf("N de la Seguridad Social: ");


scanf("%ld", &ppaciente->n_seg_social );

while(getchar()!=’\n’); /* scanf() termina de leer en cuanto termina el


número, dejando el \n del final de la cadena que
introduce el usuario en el buffer de lectura. Si
no se eliminan mediante este bucle los caracteres
que scanf no ha leı́do, dichos caracteres serán
leı́dos por gets y se almacenarán en la historia
del paciente */
printf("Historial del paciente: ");
gets(ppaciente->historia);
}

Por último, un programa que muestra cómo se llamarı́a a ambas funciones es:

#include <stdio.h>

typedef struct paciente{


char nombre[100];
unsigned short edad;
float peso;
unsigned long n_seg_social;
char historia[1000];
}PACIENTE;

void InitPaciente(PACIENTE *ppaciente);


void ImpriPaciente(PACIENTE *ppaciente);

main()
{
PACIENTE paciente;

InitPaciente(&paciente);

printf("\nLos datos del paciente son:\n\n");


ImpriPaciente(&paciente);
}

13.8. Vectores de estructuras


Supóngase que se necesita realizar un programa que calcule el producto vectorial de dos
vectores de números complejos. Una solución podrı́a ser crear cuatro vectores, dos para las
partes real e imaginaria del primer vector de números complejos y otros dos para las partes real
e imaginaria del segundo vector de complejos. Sin embargo la solución ideal a este problema
13.8. VECTORES DE ESTRUCTURAS 149

es crear dos vectores de estructuras de tipo COMPLEJO con las que se trabajará mucho más
cómodamente.
La declaración de un vector de estructuras es idéntica a la de un vector de cualquier otro
tipo de dato. Por ejemplo para crear un vector de 4 estructuras de tipo COMPLEJO basta con
escribir:

COMPLEJO vector_complejo[4];

Y para acceder a los miembros de una estructura que a su vez es un elemento del vector se
hace:

vector_complejo[0].real = 4.3;
vector_complejo[0].imag = 23.27;
...
vector_complejo[3].real = 2.1;
vector_complejo[3].imag = 3.7;

Nótese que al igual que el resto de vectores la numeración de los elementos comienza en
0.
Conviene tener en cuenta que al igual que con el resto de los vectores, al definir un vector
de estructuras se reserva un espacio suficiente en la memoria para contener a las estructuras
que componen el vector y se crea un puntero constante que apunta al comienzo de dicha zona
de memoria. En este ejemplo vector complejo es un puntero constante de tipo COMPLEJO *
que apunta al principio del vector de cuatro estructuras de tipo COMPLEJO.

13.8.1. Ejemplo: Cálculo del producto escalar de dos vectores de com-


plejos
Se desea realizar un programa que calcule el producto escalar de dos vectores de números
complejos que el usuario introducirá por el teclado. La dimensión de los vectores será variable,
pidiéndose al usuario al principio del programa.
Tal como se puede apreciar en el listado del programa, lo primero que se escribe es la
declaración de la estructura complejo junto con la definición del “alias” COMPLEJO (lı́neas
16–19). De esta forma, a partir de la lı́nea 19 el tipo COMPLEJO puede usarse al igual que el
resto de tipos de datos de C, tal como se aprecia por ejemplo en los prototipos de las funciones
(lı́neas 22 y 23).

1 /* Programa: PVecComp
2 *
3 * Descripción: Pide al usuario dos vectores de números complejos y calcula
4 * su producto escalar.
5 *
6 * Revisión 0.0: 19/05/1999
7 *
8 * Autor: El programador estructurado.
9 */
10
11 #include <stdio.h>
12
13 #define MAX_DIM 100 /* Dimensión de los vectores de complejos */
14
15 /* Declaración de una estructura para contener números complejos */
150 CAPÍTULO 13. ESTRUCTURAS DE DATOS

16 typedef struct complejo{


17 double real;
18 double imag;
19 }COMPLEJO;
20
21 /* Prototipos de las funciones */
22 void PideVec(COMPLEJO *pvec, int dim);
23 COMPLEJO ProdEsc(COMPLEJO *pvec1, COMPLEJO *pvec2, int dim);
24
25 void main(void)
26 {
27 COMPLEJO vec1[MAX_DIM], vec2[MAX_DIM]; /* Vectores introducidos por el
28 usuario */
29 COMPLEJO res; /* Resultado del producto escalar */
30 int dim; /* Dimensión de los vectores*/
31
32 printf("Introduzca la dimensión de los vectores: ");
33 scanf("%d", &dim);
34
35 printf("\nIntroduzca los complejos del vector 1\n");
36 PideVec(vec1, dim);
37
38 printf("\nIntroduzca los complejos del vector 2\n");
39 PideVec(vec2, dim);
40
41 res = ProdEsc(vec1, vec2, dim);
42
43 printf("\nEl producto escalar es: %lf + %lf i\n", res.real, res.imag);
44 }
45
46 /* Función: PideVec()
47 *
48 * Descripción: Pide al usuario un vector de números complejos de dimensión dim
49 *
50 * Argumentos: COMPLEJO *pvec1: Puntero al vector de estructuras de complejos
51 * donde se escribirán los números introducidos
52 * por el usuario.
53 * int dim: Dimensión del vector de complejos.
54 */
55
56 void PideVec(COMPLEJO *pvec, int dim)
57 {
58 int n;
59
60 for(n=0; n<dim; n++){
61 printf("Parte real del elemento %d: ", n);
62 scanf("%lf", &(pvec[n].real) );
63 printf("Parte imaginaria del elemento %d: ", n);
64 scanf("%lf", &(pvec[n].imag) );
65 }
66 }
67
68 /* Función: ProdEsc()
69 *
13.8. VECTORES DE ESTRUCTURAS 151

70 * Descripción: Calcula el producto escalar de dos vectores de números


71 * complejos
72 *
73 * Argumentos: COMPLEJO *pvec1:
74 * COMPLEJO *pvec2: Punteros a los vectores de complejos.
75 * int dim: Dimensión de los vectores de complejos.
76 *
77 * Valor devuelto: COMPLEJO: Resultado del producto escalar.
78 */
79
80 COMPLEJO ProdEsc(COMPLEJO *pvec1, COMPLEJO *pvec2, int dim)
81 {
82 int n;
83 COMPLEJO resul;
84
85 resul.real = 0; /* Inicialización del resultado */
86 resul.imag = 0;
87
88 for(n=0; n<dim; n++){
89 resul.real += pvec1[n].real*pvec2[n].real - pvec1[n].imag*pvec2[n].imag;
90 resul.imag += pvec1[n].real*pvec2[n].imag + pvec1[n].imag*pvec2[n].real;
91 }
92 return resul;
93 }

La función main() consta simplemente de las definiciones de los vectores de estructuras


(linea 27) y de las llamadas a las funciones PideVec() y ProdEsc(), que es donde se realiza
todo el proceso del programa.
La función PideVec() se encarga de pedir al usuario las partes real e imaginaria de cada
elemento del vector y de almacenarlas en vector cuya dirección se le pasa como argumento
(pvec). Nótese que se ha usado el operador corchete para acceder a cada uno de los elementos
del vector.
Por último la función ProdEsc() se encarga de calcular el producto escalar mediante la
fórmula:

dim 1


       
p ∑ ℜV1 i ℜV2 i ℑV1 i ℑV2 i   ℜV1 i ℑV2 i  ℑV1 i ℜV2 i  i

 
i 0

En donde ℜV1 i y ℑV1 i son las partes real e imaginaria del elemento i del vector de
complejos V1 .
Para calcular el sumatorio de la fórmula anterior, en cada iteración del bucle for de las
lı́neas 88–91 se calcula un sumando de dicho sumatorio y se acumula dicho sumando en el
número complejo resul, que previamente se ha inicializado a cero (lı́neas 85–86). La función
devuelve (mediante copia) el valor de dicho número complejo al programa principal.

Ejercicios
1. Modificar el programa principal para impedir que el usuario introduzca una dimen-
sión errónea para los vectores: si el usuario intenta introducir una dimensión mayor del
tamaño máximo del vector MAX DIM o menor o igual a 0, se dará un mensaje de error y
se volverá a pedir la dimensión de nuevo.
152 CAPÍTULO 13. ESTRUCTURAS DE DATOS

2. Modificar el programa principal para crear los vectores dinámicamente. Una vez cono-
cida la dimensión de estos, se asignará memoria para los dos vectores mediante una
llamada a la función calloc().
3. Modificar la función ProdEsc() para trabajar con el operador flecha -> en lugar del
operador corchete [] para acceder a los elementos de los vectores de complejos en el
cálculo del producto escalar.

13.9. Estructuras y archivos


Existen dos tipos de archivos en función del formato de almacenamiento: los archivos de
texto y los archivos binarios.
Hasta ahora sólo se han visto los archivos de texto ya que son los más utilizados, los más
sencillos y valen para almacenar variables de los tipos básicos (int, float, double...). Para
almacenar estructuras en archivos también es posible utilizar archivos de texto, como se verá a
continuación, sin embargo lo más normal en este caso es utilizar archivos binarios.

13.9.1. Diferencia entre archivos de texto y archivos binarios


Los archivos de texto sólo contienen caracteres imprimibles como letras, números y signos
de puntuación. Estos archivos se pueden abrir y modificar con un editor de texto como por
ejemplo el ”Bloc de notas”de Windows. Normalmente se escriben con la función fprintf que
se encarga de convertir las variables numéricas a caracteres de acuerdo al formato especificado.
Por ejemplo si una variable entera vale 2563, al escribir el valor de esta variable en pantalla
mediante la función printf o en un archivo mediante la función fprintf se obtiene una
secuencia de caracteres que depende de la especificación de formato. Con formato " %5d"
se generan los 5 caracteres ’ ’, ’2’, ’5’, ’6’ y ’3’; mientras que con formato " %06d" se
generan los caracteres ’0’, ’0’, ’2’, ’5’, ’6’ y ’3’.

a=2563;
printf("a vale:%5d\n",a); ---> a vale: 2563
printf("a vale:%06d\n",a); ---> a vale:002563

En definitiva los archivos de tipo texto son legibles porque sólo contienen caracteres como
números, letras y signos de puntuación. Las funciones fprintf y fscanf en encargan de
hacer las traducciones de formato numérico a caracteres y viceversa.
Los archivos binarios se utilizan para almacenar la información como una copia exacta de
la memoria del ordenador. Por ejemplo, si una variable entera vale 2563 estará almacenada en
la memoria del ordenador en forma de 2 bytes (en un PC con Windows sizeof(int) vale 2).
De acuerdo a la codificación de los números enteros estos dos bytes serán (en hexadecimal) el
0A y el 03 o lo que es igual el 0000 1010 y el 0000 0011 en formato binario. Estos bits son
los que realmente se encuentran almacenados en la memoria.
Cuanto este número entero se guarda en un archivo binario se guardan 2 bytes dentro del
archivo (como ya se ha dicho serán los bytes 0A y 03). Como norma general los archivos
binarios no se pueden ver con un editor de texto, ya que serı́a una casualidad que los bytes
que contienen fuesen caracteres legibles. Siguiendo con el ejemplo anterior, ni el carácter 0A
ni el carácter 03 son imprimibles. También se puede decir que los archivos binarios y los
programas que utilizan archivos binarios no son fácilmente portables a otros ordenadores u
otros compiladores. Esto es evidente ya que el tamaño de las variables no es siempre igual, una
variable tipo int ocupa 2 bytes en un PC con sistema operativo Windows, pero normalmente
13.9. ESTRUCTURAS Y ARCHIVOS 153

ocupa 4 bytes en una estación de trabajo con sistema operativo Unix. Además, no es sólo un
problema de tamaño sino que unos procesadores ordenan la memoria de una manera (primero
0A y luego 03) y otros al revés (primero 03 y luego 0A). Es importante tener en cuenta todas
estas caracterı́sticas de los archivos binarios cuando se adaptan programas que funcionan en
una configuración determinada a otra configuración distinta.

13.9.2. Para almacenar estructuras en archivos de texto


Los miembros de una estructura pueden almacenarse individualmente dentro de un archivo
de texto mediante la función fprintf. Para ello hay que abrir el archivo, escribir el valor de
cada estructura (campo por campo) y cerrar el archivo. Ver el siguiente programa de ejemplo
donde se utilizan estructuras con el nombre y el precio de 2 productos.

/*
Programa: texto.c
Descripción: Inicializa un vector de dos elementos tipo estructura y escribe
todos los datos en un archivo de texto.
Revisión 0.0: 1/jun/1999
Autor: El programador de texto.
*/
#include <stdio.h> /* printf, fprintf, fopen, fclose */
#include <stdlib.h> /* exit */
#include <string.h> /* strcpy */

#define N 2 /* Dimensión de los vectores (número de productos) */

/* Declaración de una estructura para los productos */


typedef struct {
char nombre[12];
int precio;
}PROD;

void main(void)
{
PROD producto[N]; /* Vector de productos en el almacén */
int i; /* contador para los bucles*/
FILE *f; /* Descriptor del archivo de texto */

/*** Inicializo del vector ***/


strcpy(producto[0].nombre,"CD Rosana");
producto[0].precio=2563;
strcpy(producto[1].nombre,"CD Titanic");
producto[1].precio=2628;

/*** Creo el archivo ***/


f=fopen("datos.txt","w");
if (f==NULL) {
printf("Error al crear el archivo datos.txt\n");
exit(1);
}
154 CAPÍTULO 13. ESTRUCTURAS DE DATOS

for (i=0; i<N; i++) {


fprintf(f,"%s\n",producto[i].nombre);
fprintf(f,"%6d\n",producto[i].precio);
}
fclose(f);
}

El archivos datos.txt que produce el programa anterior tiene cuatro lı́neas porque se ha
puesto un carácter de cambio de lı́nea al final de cada instrucción fprintf. El archivo tiene el
siguiente aspecto:

CD Rosana
2563
CD Titanic
2628

Los números no aparecen ajustados al margen izquierdo porque se ha puesto un forma-


to %6d que escribe dos espacios delante de cada número de cuatro cifras. El archivo tiene en
total 35 caracteres que son los siguientes:

CD Rosana 2563 CD Titanic 2628

Nota: Se han representado los espacios como y los caracteres de cambio de lı́nea como
para que se pueda leer mejor.

Ejercicios
1. Comprobar que cada estructura escribe un número diferente de caracteres en el archivo.
2. ¿Cuál serı́a el archivo resultante si los fprintf fuesen los siguientes?:

fprintf(f,"%15s\n",producto[i].nombre);
fprintf(f,"%-6d\n",producto[i].precio);

¿Cuántos caracteres ocuparı́a el archivo?

13.9.3. Para almacenar estructuras en archivos binarios


Las estructuras pueden almacenarse dentro de un archivo binario mediante la función
fwrite. Para ello hay que abrir el archivo en modo binario, escribir las estructuras y cer-
rar el archivo. La función fwrite no utiliza especificadores de formato sino que realiza un
volcado de la memoria en el archivo. El prototipo de la función fwrite es:

size_t fwrite(void *estructura, size_t tamaNo, size_t numero, FILE *archivo);

Donde size t es equivalente a unsigned long int, es decir se refiere a un número, y


void * es un puntero genérico que vale para cualquier tipo de estructura. Por lo tanto, esta
función recibe como argumentos un puntero a una estructura (la dirección de memoria de
la estructura), luego el tamaño de la estructura en bytes (que se obtiene con sizeof()), el
número de estructuras que queremos guardar y finalmente el descriptor del archivos en el cual
queremos escribir.
Ver el siguiente programa de ejemplo que es equivalente al programa del apartado anterior.
13.9. ESTRUCTURAS Y ARCHIVOS 155

/*
Programa: binario.c
Descripción: Inicializa un vector de dos elementos tipo estructura y escribe
todos los datos en un archivo binario.
Revisión 0.0: 1/jun/1999
Autor: El programador binario.
*/
#include <stdio.h> /* printf, fwrite, fopen, fclose */
#include <stdlib.h> /* exit */
#include <string.h> /* strcpy */

#define N 2 /* Dimensión de los vectores (número de productos) */

/* Declaración de una estructura para los productos */


typedef struct {
char nombre[12];
int precio;
}PROD;

void main(void)
{
PROD producto[N]; /* Vector de productos en el almacén */
int i; /* contador para los bucles*/
FILE *f; /* Descriptor del archivo de texto */

/*** Inicializo del vector ***/


strcpy(producto[0].nombre,"CD Rosana");
producto[0].precio=2563;
strcpy(producto[1].nombre,"CD Titanic");
producto[1].precio=2628;

/*** Creo el archivo ***/


f=fopen("datos.dat","wb");
if (f==NULL) {
printf("Error al crear el archivo datos.dat\n");
exit(1);
}
for (i=0; i<N; i++) {
fwrite(&producto[i], sizeof(producto[i]),1,f);
}
fclose(f);
}

Notar que al abrir el archivo se especifica el modo wb, la b indica acceso binario. El
archivos datos.dat que produce el programa anterior no puede verse en un editor de tex-
to porque contiene caracteres especiales. Ocupa en total 28 caracteres ya que cada estructura
ocupa 14 bytes, que son 12 caracteres del primer nombre, y 2 bytes para el int del precio. Por
lo tanto el archivo puede representarse como:

CD Rosana ????CD Titanic ???


156 CAPÍTULO 13. ESTRUCTURAS DE DATOS

Nota: Se han representado los espacios como , los caracteres ’\0’ de final de cadena
como y otros caracteres no imprimibles como ?. En este caso no hay cambios de lı́nea. Los
12 primeros caracteres corresponden al nombre del primer producto, que es un vector de char
de longitud 12 de los cuales sólo 10 caracteres están inicializados (9 de textttCD Rosana y el
10o el carácter NULL que pone la función strcpy) por lo tanto luego aparecen 2 caracteres
desconocidos. A continuación viene el número 2563 codificado como entero que ocupa 2 bytes
(que son el 0A y el 03 en hexadecimal). Otra manera de representar el contenido del archivo
es la siguiente:
’C’ ’D’ ’ ’ ’R’ ’o’ ’s’ ’a’ ’n’ ’a’ ’\0’ ??? ??? 0A 03
’C’ ’D’ ’ ’ ’T’ ’i’ ’t’ ’a’ ’n’ ’i’ ’c’ ’\0’ ??? 0A 44

Ejercicio
1. Comprobar que en lugar del bucle for se podı́a haber escrito la siguiente lı́nea:

fwrite(&producto[0], sizeof(producto[0]),N,f);

Esta instrucción escribe las N estructuras del vector producto de una sola vez.
2. ¿Cuántos caracteres ocuparı́a el archivo si el precio se guardase en una variable tipo
long en lugar de tipo int?
3. ¿Cuál serı́a el archivo resultante si el nombre del primer producto fuese ÇD-R" y el
precio 200?

13.9.4. Funciones fread, fwrite y fseek


Para leer estructuras de un archivo binario se utiliza la función fread que es equivalente
a la función fwrite que se ha visto anteriormente. Los prototipos de estas funciones son los
siguiente:

size_t fread (void *estructura, size_t tamaNo, size_t numero, FILE *archivo);
size_t fwrite(void *estructura, size_t tamaNo, size_t numero, FILE *archivo);

Ambas funciones hacen un acceso directo a la memoria del ordenador a partir de la direc-
ción de la estructura. La función fread lee del archivo tamaNo*numero bytes y los guarda a
partir de la dirección de memoria estructura. Por el contrario, la función fwrite toma los
bytes de la memoria y los escribe en el archivo. En ambos casos las funciones devuelven un
valor de tipo size t que es el número de estructuras que han sido capaces de leer o de escribir.
El valor devuelto suele ser igual al valor solicitado (numero) salvo que se produzca un error.
En el caso de escritura es muy raro que se produzca un error, pero en caso de lectura permite
localizar el final del archivo.
Una de las caracterı́sticas más importantes de los archivo binarios de estructuras es que
cada registro ocupa un espacio constante, que es el tamaño de la estructura. Esto permite
avanzar o retroceder en el archivo para ir a leer un registro concreto, es decir, se permite el
acceso directo a los datos. La función que permite moverse en un archivo a una posición
concreta se llama fseek y tiene el siguiente prototipo:

size_t fseek(FILE *archivo, long posicion, int origen);

Donde origen especifica desde dónde se calcula la posición. Puede valer:


13.9. ESTRUCTURAS Y ARCHIVOS 157

1. SEEK SET desde el principio del archivo. Es la opción más normal.


2. SEEK CUR desde la posición actual. Permite avanzar o retroceder.
3. SEEK END desde el final del archivo. Permite ir rápidamente al final del archivo.
Ejemplo:
fseek(fp,1,SEEK_SET); /* Va a la primera posición del archivo */
fseek(fp,10*sizeof(producto[0]),SEEK_SET); /* Va al 10o producto */
fseek(fp,2*sizeof(producto[0]),SEEK_CUR); /* Me salto dos productos */
158 CAPÍTULO 13. ESTRUCTURAS DE DATOS
Bibliografı́a

[Antonakos and Mansfield, 1997] Antonakos, J. L. and Mansfield, K. C. (1997). Progra-


mación estructurada en C. Prentice Hall Iberia, Madrid.
[Delannuy, ] Delannuy, C. El Libro de C como Primer Lenguaje. Eyrolles, ediciones Gestión
2000 S. A., Barcelona.
[Kernighan and Ritchie, ] Kernighan, B. W. and Ritchie, D. M. El lenguaje de programaci ón
C. Prentice Hall, segunda edición.

[Roberts, 1995] Roberts, E. S. (1995). The Art and Science of C. Addison Wesley.

159

También podría gustarte