Apuntes Completos de C++
Apuntes Completos de C++
Apuntes Completos de C++
Fundamentos de informática
2 de abril de 2002
2
Índice general
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
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
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
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
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
a b
7
8 CAPÍTULO 1. DESCRIPCIÓN DEL ORDENADOR
Bus de direcciones
Bus de control
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.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.
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:
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).
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:
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
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.
17
18 CAPÍTULO 2. EL PRIMER PROGRAMA EN C
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.
/* 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
*/
main(void)
{
printf("Hola!\n"); /* Imprimo el mensaje */
}
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 */
/* Este
es
el
fichero
stdio.h*/
/* 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 */
}
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
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.
25
26 CAPÍTULO 3. TIPOS DE DATOS
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;
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
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.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;
#include <stdio.h>
main(void)
{
int dia;
dia=47;
30 CAPÍTULO 3. TIPOS DE DATOS
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:
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:
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);
}
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.
Los tipos de variables derivados de int, como short, long, unsigned. . . utilizan los carac-
teres de formato que se muestran en la siguiente tabla:
%u unsigned int
%hu unsigned short
%lu unsigned long
/*********************
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);
}
#include <stdio.h>
main(void)
{
char c;
c=’z’;
printf("Carácter %c\n",c);
printf("Valor %d\n",c);
}
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
/*********************
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;
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.
variable = expresión;
35
36 CAPÍTULO 4. OPERADORES EN C
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:
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.
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;
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.
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.
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:
Como el alumno aventajado habrá notado sólo serı́a necesario poner un molde en cualquiera
de las dos variables enteras ¿Por qué?
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;
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
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");
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.
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 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.
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
Ejercicio
1. Comprobar que otra forma de escribir el bucle del ejemplo serı́a poner for(i=0; i!=3;
i++).
#include <stdio.h>
#include <robot.h> /*Declara las funciones para el manejo del robot*/
main(void)
{
int i,j;
....................
...R****************
********************
********************
********************
*/
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
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
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
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.
/* 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*/
}
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
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 */
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 }
do{
instrucción 1;
instrucción 2;
...
instrucción n;
}while(condición);
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
hacer{
pedir número;
sumar el número a la suma anterior;
}mientras número no sea cero;
imprimir total;
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
while(condición){
instrucción 1;
instrucción 2;
...
instrucción n;
}
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:
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
2
1
n
nn
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
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.
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;
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
#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);
}
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==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
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");
}
/*********************
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;
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;
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;
}
#include <stdio.h>
66 CAPÍTULO 7. CONTROL DE FLUJO: IF
main(void)
{
double 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");
}
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
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.
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:
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.
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
8.2.3. Ejemplos
Veamos a continuación algunos ejemplos para ilustrar el uso de la construcción switch-case.
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;
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.
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.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*/
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.
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*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
main(void)
{
double num; /* número usado para recorrer 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.
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 .
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
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]);
}
}
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.
/*********************
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:
/*********************
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.
/*********************
Programa: cadenas4.c
Descripción: Inicializa un vector de char y luego escribe
9.3. CADENAS DE CARACTERES 87
#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");
}
/*********************
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");
}
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).
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.
i=strcmp(nombre1,nombre2);
que cuaquier otro carácter normal. Por ejemplo: 3Com < Andalucı́a < Sevilla < boreal <
nórdico < África
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
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");
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
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
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:
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;
/*********************
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;
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 */
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
double Cuad(double x)
{
return x*x;
}
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
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. . .
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 }.
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,
#define.
100 CAPÍTULO 10. FUNCIONES
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.
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
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++;
}
...
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
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:
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á:
y su definición serı́a:
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
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:
10.8. Ejemplos
Veamos a continuación algunos ejemplos más para aclarar ideas.
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.
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?
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:
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 }
vect1[0] = 1;
vect1[2] = 2;
vect1[3] = 3;
Ası́, si se generaliza la función ProdEscalar() para trabajar con vectores de cualquier di-
mensión, el programa anterior queda:
y su definición es:
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);
...
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
3. El prototipo será:
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.
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
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.
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.
ii=78; /* inicializo ii */
pti=ⅈ /* 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
? 34FF pti
34FF 78 ii
Figura 11.2:
Ejercicios
1. ¿Sabrı́a explicar por qué el programa anterior imprime un valor erróneo?
351F ????
Figura 11.3:
*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).
pti++
ptd++
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 */
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:
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".
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;
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:
punt+=4;
*punt=45;
punt-=4;
*(punt+4)=45;
punt[4]=45;
/* 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
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.
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:
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.
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;
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:
free(pent);
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:
-----------------------
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.
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 .
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.
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
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
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 }
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
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:
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
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", ¬a_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", ¬a_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);
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
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:
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.
141
142 CAPÍTULO 13. ESTRUCTURAS DE DATOS
};
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];
};
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:
WORD edad;
En lugar de:
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:
PACIENTE pepito;
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
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 }
confusiones.
13.6. ESTRUCTURAS Y FUNCIONES 145
...
struct paciente pepito;
struct paciente copia_de_pepito;
...
/* aquı́ estarı́a la inicialización de pepito */
...
copia_de_pepito = pepito;
resultado.real = real;
resultado.imag = imag;
return resultado;
}
...
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;
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.
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;
Y una función para inicializar una estructura del tipo PACIENTE pidiéndole los datos al
usuario serı́a:
Por último, un programa que muestra cómo se llamarı́a a ambas funciones es:
#include <stdio.h>
main()
{
PACIENTE paciente;
InitPaciente(&paciente);
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.
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
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.
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.
/*
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 */
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 */
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
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);
/*
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 */
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 */
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:
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?
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:
[Roberts, 1995] Roberts, E. S. (1995). The Art and Science of C. Addison Wesley.
159