tema 6: introducción a la programación iibiolab.uspceu.com/aotero/recursos/docencia/tema 6.pdf ·...
TRANSCRIPT
Tema 6:
Introducción a la programación II
Objetivos: en este tema nos aventaremos en profundidad en el lenguaje de
programación C. Sentencias de control de flujo, funciones de entrada y salida
para la pantalla y para ficheros, y diversas estructuras de datos son los
principales puntos que se abordarán.
Programación 2/92
Índice
Índice ...........................................................................................................................................2
1 Funciones de entrada y salida..............................................................................................6
1.1 getchar .........................................................................................................................6
1.2 putchar .........................................................................................................................7
1.3 printf ............................................................................................................................7
1.3.1 Aspectos avanzados de printf ............................................................................10
1.4 scanf...........................................................................................................................12
1.4.1 Aspectos avanzados de scanf.............................................................................13
1.5 gets.............................................................................................................................15
1.6 puts ............................................................................................................................15
2 Borrado de pantalla y colocación del cursor .....................................................................15
3 Punteros .............................................................................................................................16
3.1 Aritmética de punteros ..............................................................................................19
3.2 Punteros NULL y void ..............................................................................................20
3.3 Asignación dinámica de memoria .............................................................................21
3.3.1 Liberación de la memoria. .................................................................................22
3.3.2 Función realloc ..................................................................................................23
Programación 3/92
3.4 Punteros constantes vs punteros a constantes............................................................24
3.5 Punteros de funciones a otras funciones....................................................................26
4 Arrays ................................................................................................................................27
4.1 Inicialización de Arrays.............................................................................................30
4.2 Punteros y arrays unidimensionales ..........................................................................30
4.3 Programas de ejemplo ...............................................................................................32
4.4 Arrays y reserva dinámica de memoria .....................................................................34
4.5 Punteros y arrays multidimensionales .......................................................................36
5 Cadenas de caracteres........................................................................................................39
5.1 Declaración de una cadena ........................................................................................40
5.2 Inicialización de cadenas ...........................................................................................40
5.3 Lectura y escritura de cadenas...................................................................................41
5.4 Funciones para trabajar con cadenas .........................................................................42
5.5 Arrays de cadenas......................................................................................................45
5.6 Punteros a cadenas.....................................................................................................46
6 Estructuras .........................................................................................................................48
6.1 Tipos de datos definidos por el usuario .....................................................................54
6.2 Estructuras y punteros ...............................................................................................55
7 Uniones..............................................................................................................................57
Programación 4/92
8 Archivos de datos ..............................................................................................................59
8.1 Apertura y cierre de un archivo .................................................................................60
8.2 Lectura y escritura de un archivo de texto.................................................................63
8.3 Lectura y escritura de bloques ...................................................................................66
8.3.1 feof.....................................................................................................................69
8.3.2 rewind ................................................................................................................69
8.3.3 fseek...................................................................................................................70
8.3.4 ftell.....................................................................................................................70
8.4 Ejemplo de entrada y salida.......................................................................................70
9 Enumeraciones ..................................................................................................................77
10 Macros ...........................................................................................................................79
11 Más directivas del preprocesador ..................................................................................82
11.1 #define .......................................................................................................................82
11.2 #undef ........................................................................................................................82
11.3 #if Inclusión condicional ...........................................................................................83
11.4 Control del preprocesador del compilador ................................................................84
11.5 Otras directivas del preprocesador ............................................................................85
12 Parámetros de línea de comandos..................................................................................85
13 Ejercicios .......................................................................................................................88
Programación 5/92
13.1 Entrada y salida de pantalla .......................................................................................88
13.2 Punteros .....................................................................................................................88
13.3 Arrays ........................................................................................................................89
13.4 Cadenas de caracteres................................................................................................90
13.5 Estructuras y uniones.................................................................................................91
13.6 Entrada y salida .........................................................................................................91
Programación 6/92
1 Funciones de entrada y salida
Para comunicarse con el exterior un programa necesita mostrar mensajes, resultados
intermedios, etc. así como solicitar del usuario los datos de entrada del programa. En C
existen una serie de funciones que permiten leer y escribir datos de la entrada estándar
(habitualmente el teclado) y la salida estándar (habitualmente el monitor) del ordenador:
getchar, putchar, gets, puts, scanf y printf. Estas funciones están definidas en el fichero de
cabecera stdio.h (STanDard Input Output, entrada y salida estándar) por lo que los
programas que las utilicen deberán tener al comienzo la directiva #include <stdio.h>.
Antes de ver las funciones de C, es conveniente conocer algunos detalles acerca del
funcionamiento de la pantalla de un ordenador. Las pantallas están divididas en filas y
columnas. La pantalla de un PC en modo texto normal tiene 25 filas y 80 columnas. En la
pantalla suele verse un guión parpadeante, el cursor, que indica donde va a aparecer el
siguiente carácter que se escriba. Las funciones de E/S de C escriben siempre a partir de la
posición del cursor. Cuando se sobrepasa la columna 80, se comienza a escribir en la
columna 1 de la línea siguiente. Al sobrepasar la línea 25 de pantalla, todas las líneas se
desplazan 1 posición hacia arriba y aparece una nueva línea 25. El contenido de la línea 1
puede desaparecer.
1.1 getchar
Permite la entrada de caracteres de uno en uno del dispositivo de entrada estándar
(típicamente teclado). No requiere argumentos pero es necesario escribir un par de
paréntesis vacíos tras la palabra getchar, al igual que sucede con cualquier función que no
recibe argumentos. Un ejemplo de su uso es:
char c = getchar();
Programación 7/92
Si se está leyendo de un fichero y se encuentra el final del fichero, la función getchar
devuelve la constante simbólica EOF, definida en stdio.h.
1.2 putchar
Visualiza un carácter por la salida estándar (típicamente la pantalla).
char c =’q’; putchar(c) ;
1.3 printf
La función printf se utiliza para escribir en pantalla mensajes y valores de expresiones. Su
formato general es:
printf (“string de mensaje y formato”, expresión1, expresión2, ..., expresiónN);
Un string es un conjunto de caracteres encerrados entre comillas dobles (" "). El string
incluido en printf sale escrito en pantalla “tal cual”, salvo ciertos caracteres especiales, que
se utilizan para indicar cambios de línea, caracteres que no existen en el teclado y para
indicar dónde y cómo se desean imprimir valores de expresiones o variables. Para imprimir
simplemente un mensaje de texto se usa una sentencia como la siguiente:
printf("Esto es un mensaje");
Esta sentencia hace aparecer en pantalla el texto:
Esto es un mensaje
Se pueden utilizar códigos de escape para realizar saltos de linea o imprimir caracteres
especiales. Un código de escape comienza por el carácter \. Los códigos de escape que se
pueden utilizar se muestran en la tabla que sigue. No todos los códigos de escape
funcionan en cualquier dispositivo de salida, por ejemplo, el de avance de página sólo
funciona en impresoras.
Programación 8/92
• \\ \ (barra)
• \a beep (sonido)
• \b retrocede una posición borrando el carácter
• \f avance de página (impresora)
• \n nueva línea
• \r retorno de carro
• \t tabulador horizontal
• \v tabulador vertical (impresora)
• \? interrogación (no es necesario en MS-DOS)
• \' comilla simple (no es necesario en MS-DOS)
• \" comilla doble
• \0oo carácter cuyo cod. ASCII es oo en octal
• \xhh carácter cuyo cod. ASCII es hh en hexadecimal
Estos códigos pueden colocarse en cualquier parte del string y repetirse o combinarse
como se desee, por ejemplo:
printf("Esta es una linea\n\ny esta va detras");
Hace aparecer en pantalla:
Esto es una linea y esta va detras
Para que un printf escriba valores de expresiones es necesario decir, por una parte, qué
expresiones hay que escribir y por otra, cómo y donde queremos que aparezcan sus
valores. Las variables o expresiones de las que se quieren imprimir sus valores se colocan
a la derecha del string, separadas por comas. Para indicar donde y cómo queremos que
aparezca su valor dentro del mensaje, se coloca en el string un especificador de formato,
que consiste en un carácter % seguido de una secuencia de una o más letras y números. El
especificador de formato indica el tipo (carácter, número, etc.) del valor a imprimir y la
Programación 9/92
forma de imprimirlo. El especificador debe ser compatible con el tipo de la variable o
expresión. Los tipos básicos se muestran a continuación:
• int %i o %d (entero decimal con signo)
• int %o (entero octal)
• int %x (entero hexadecimal)
• int %u (entero decimal sin signo)
• float %f (no escribe el exponente)
• float %e (escribe el exponente)
• float %g (utiliza %f o %e según sea el caso)
• char %c
• string %s
• puntero %p
La mayoría de las versiones de C permiten anteponer una l para un entero largo (%ld) o
para un double (%lf). %hd suele indicar un entero corto. Si se escriben en mayúscula los
caracteres de conversión x, e y g entonces cualquier letra de los datos de salida se
presentará en mayúscula. Veamos un ejemplo:
pres1=4.5661; printf("La presion vale %f kg/cm2",pres1);
Hace aparecer en pantalla:
La presion vale 4.5661 kg/cm2
Para imprimir varias expresiones a la vez, se coloca un especificador de formato para cada
una. El compilador asocia la primera expresión al primer especificador de formato, la
segunda expresión al segundo especificador de formato, etc.
printf("El medidor %d da un valor de %f gramos", med, peso");
Programación 10/92
Si un string es largo y hace que el final de una sentencia quede fuera de la pantalla al editar
el programa, puede cortarse en varios trozos, que se colocan uno después de otro, sin
comas, pero encerrando todos los fragmentos de la cadena de caracteres entre comillas:
printf("Este es un string muy largo y esta cortado " "en dos trozos");
Hace parecer en pantalla:
Este es un string muy largo y esta cortado en dos trozos
La función printf devuelve como resultado de su ejecución un número entero. Si no ha
habido ningún problema, es el número de caracteres que ha impreso. Si el número devuelto
es -1 ha habido algún problema al imprimir algún valor.
1.3.1 Aspectos avanzados de printf
printf permite especificar una longitud de campo mínima. Si el número de caracteres del
dato correspondiente es menor que la longitud de campo especificada, entonces el dato
será precedido por los espacios en blanco necesarios para que se consiga la longitud de
campo especificada. Si el número de caracteres del dato excede la longitud del campo
especificada, se visualizará el dato completo:
int i=12345; ......... printf("%3d %5d %8d\n\n",i,i,i);
Hace aparecer en pantalla:
12345 12345 12345
También es posible especificar el máximo número de cifras decimales para un valor en
coma flotante o el máximo número de caracteres para una cadena de caracteres. A este
parámetro se le llama precisión. La precisión se especifica mediante un entero sin signo
que es precedido por un punto decimal. El número en coma flotante se redondeará en caso
de que deba recortarse para ajustarse a la precisión especificada:
float x=123.456; printf("%7f %7.3f %7.1f\n\n",x,x,x);
Programación 11/92
Hace aparecer en pantalla:
123.456000 123.456 123.5
Cuando la longitud de campo mínima se aplica a una cadena de caracteres y la cadena es
más corta que la precisión, se añaden espacios en blanco. Si una cadena de caracteres
excede la precisión, no se muestran los caracteres sobrantes de la parte derecha de la
cadena:
char linea[12]="hexadecimal"; printf("%10s %15s %15.5s %.5s",linea,linea,linea,linea);
Hace aparecer en pantalla:
hexadecimal hexadecimal hexad hexad
Cada grupo de caracteres dentro de la cadena de control puede incluir un indicador que
afecta a cómo aparece la salida. Este indicador debe aparecer inmediatamente después del
signo de porcentaje. A continuación mostramos los indicadores que existen junto con su
función:
• - El dato se ajusta a la izquierda dentro del campo (si se requieren espacios en
blanco para conseguir la longitud de campo mínima).
• + El dato se ajusta la derecha dentro del campo.
• 0 Hace que se presenten ceros en lugar de espacios en blanco (sólo para datos
ajustados a la derecha).
• ' ' (Espacio blanco) Cada dato numérico positivo es precedido por un espacio en
blanco.
• # (para las conversiones o y x) hace que los datos octales y hexadecimales sean
precedidos por 0 y 0x respectivamente.
• # (para las conversiones de tipo g) todos los números en coma flotante se
presentan con un punto, aunque tengan un valor entero.
Programación 12/92
1.4 scanf
La función scanf se utiliza para leer datos del teclado y almacenarlos en variables. Su
formato general es:
scanf (“string de formato”, puntero1, puntero2, ..., punteroN);
A diferencia de printf esta función no muestra ningún mensaje para el usuario. En el string
sólo debe haber un especificador de formato para cada valor que se quiera leer; si es
necesario un mensaje previo al usuario (normalmente lo es) habrá que mostrarlo mediante
la función printf. Si se ponen varios especificadores de formato, pueden colocarse sin
separación o separados por un espacio en blanco.
En lugar de utilizar las variables, esta función necesita punteros a las mismas. Un puntero a
una variable es su dirección en memoria, y se obtiene colocando el carácter & antes del
nombre de la variable. Por ejemplo, para obtener un puntero a la variable Numero se pone
&Numero. La función printf no modifica los valores de las variables que recibe, por tanto,
recibe una copia de los valores de dichas variables. Por el contrario, scanf debe modificar
dichas variables, por tanto, tiene que recibir las direcciones de memoria de las variables,
para poder acceder a ellas y modificarlas. Para la función de impresión en pantalla basta
con que las variables pasen por valor, mientras que para la de lectura, ya que va a
modificar el valor de la variable, requiere que se le pasen por referencia.
Es necesario indicar un especificador de formato para cada variable a leer. Éstos se
asocian por orden, al igual que en printf:
printf("Introduzca el medidor y la presion registrada : "); scanf("%d%f",&medidor,&presion);
En este código, el primer dato introducido debe ser un entero y se corresponde con la
variable medidor y el segundo debe ser un número real y se corresponde con la variable
presión. Cuando el usuario introduce los datos por teclado los datos estarán separados entre
sí por caracteres de espaciado (que pueden ser espacios, tabuladores o saltos de línea). Tras
una o varias lecturas de datos pueden quedar datos en el buffer de entrada que queramos
Programación 13/92
desechar (esto es, que en un futuro scanf no las lea). Para ello debemos escribir un espacio
después de las comillas del scanf:
scanf(" %d...",...)
Mostramos mediante un ejemplo el uso de printf y scanf. El programa que presentamos a
continuación calcula el cuadrado de un número introducido por el usuario. Sólo se necesita
una variable para guardar el número, ya que el cuadrado no se almacena, simplemente se
calcula su valor en el momento en que se va a escribir.
/* *ejemplo6_1.c */ #include <stdio.h> int Numero; /* declaracion de una variable entera */ main() { printf("\n\nTeclea un numero "); scanf("%d",&Numero); /* lee el numero del teclado */ printf("\nSu cuadrado es %d\n",Numero*Numero); system("pause"); }
Es necesario incluir el fichero stdio.h porque contiene una declaración de las funciones
printf y scanf. Los códigos \n se utilizan para producir los saltos de línea necesarios para
que la presentación en pantalla sea más legible.
1.4.1 Aspectos avanzados de scanf
Si queremos leer una cadena de caracteres debemos tener en cuenta que scanf al encontrar
un carácter de espaciado pasa a leer el siguiente elemento especificado en la cadena de
conversión.
Esto es, no podríamos leer una cadena que tenga caracteres de espaciado. Para poder
hacerlo usamos en lugar de %s, una secuencia de caracteres que vamos a leer entre [....].
Por ejemplo:
scanf ("%[ ABCDEFGHIJKLMNOPQRSTUVWXYZ]" , &linea)
Programación 14/92
de tal forma que si introducimos la cadena CIUDAD DE ALMERIA toda ella se introduce
en la variable linea. Por contra, si escribimos "Ciudad de Almería " sólo el carácter C se
introduce en la variable linea. Otra forma es indicar los caracteres tras los cuales debemos
parar. Esto se hace con el acento circunflejo (^):
scanf(" %[^\n]", &linea);
Esto significa que metemos en linea todos los caracteres que en leamos mientras no
encontremos el carácter \n.
La función scanf permite especificar una longitud de campo máxima. Para ello se añade un
entero sin signo después del % que indica la longitud del campo. Aunque haya más
caracteres que no sean de espaciado, éstos quedan para la siguiente lectura:
int a,b,c; scanf("%3d %3d %3d", &a,&b,&c);
Si introducimos por teclado "1 2 3 " se almacenan los siguientes valores en el variables:
a=1, b=2 y c=3. Si introducimos los datos "123 456 789" se almacenan los valores a=123,
b=456 y c=789. Si introducimos "123456789" se almacenan los valores a=123, b=456 y
c=789. Si introducimos "1234 5678 9" se almacenan los valores a=123 b=4 y c=567.
También es posible saltarse un dato sin que sea asignado a una variable. Para ello se pone
un asterisco tras el %:
scanf(" %s %*d %f", &concepto, &no_partida, &coste);
Esta instrucción no asigna ningún valor a no_partida. Si introducimos caracteres que no
sean especificadores en el formato de la cadena de scanf, éstos deben ser escritos tal cual
por el usuario cuando introduzca los datos:
int i; float f; scanf ("%d a %f", &i, &x);
los datos de entrada deben ser del tipo "1 a 2.0". Si escribimos "1 2.0" es la ejecución de
scanf termina pues no encontró el carácter esperado (a).
Programación 15/92
La función scanf devuelve como resultado el número de variables a las que se les ha
asignado un valor. Si se produjo algún error el valor de retorno será -1. Este resultado
puede usarse para comprobar que realmente hemos leído tantos datos como necesitábamos:
int datos_leidos; datos_leidos=scanf ("%d a %f", &i, &x); if (datos_leidos != 2) printf("error en la lectura\n"); else { ....... }
1.5 gets
Facilita la lectura de una cadena de caracteres de la entrada estándar. Acepta un argumento
que representa una cadena de caracteres, sin importar si esta tiene o no espacios en blanco:
char linea[80]; gets(linea);
1.6 puts
Escribe la cadena que le proporcionamos por pantalla:
char linea[80]="Hola amigo"; puts(linea);
2 Borrado de pantalla y colocación del cursor
Para mejorar la presentación en pantalla de los programas es necesario borrar de vez en
cuando el contenido de la pantalla e imprimir contenido en ella del modo más elegante
posible. Para ello pueden utilizarse estas dos funciones:
• clrscr() borra la pantalla y coloca el cursor en la esquina superior izquierda,
posición (1,1), de la pantalla.
Programación 16/92
• gotoxy(col,fil) permite colocar el cursor en la columna col y la fila fil. El printf o
scanf después de la llamada a gotoxy se realiza a partir de la posición en que queda
el cursor. Las columnas van de la 1 a la 80 y las filas de la 1 a la 25.
Estas dos funciones necesitan que se incluya el fichero conio.h, una librería no estándar
que sólo está disponible en algunos compiladores C para MS DOS. Como ejemplo se
muestran las instrucciones para una pantalla inicial de programa:
/* *ejemplo6_2.c */ #include <stdio.h> #include "conio.h" main() { clrscr(); gotoxy(30,11);printf("CALCULO DE MECANISMOS"); gotoxy(34,13);printf("Version 1.5"); }
Una forma más portable de borrar la pantalla es emplear la sentencia " system("cls");" en
MS DOS y "system("clear");" en Linux. En lo relativo al posicionamiento del cursor en la
pantalla (en modo consola) para obtener un nivel de portabilidad aceptable no hay más
alternativa que posicionarlo manualmente imprimiendo retornos de carro, espacios en
blanco y tabulaciones.
3 Punteros
Cualquier dato almacenado en la memoria está situado en una dirección de memoria y es
manejado por medio de un identificador (variable, constante, etc.). El tipo del dato
determina la longitud (número de celdas de memoria) así como la interpretación que se
debe hacer de esas celdas.
Los punteros son un tipo de variable y, como tal, su función es almacenar información. La
principal diferencia con el resto de los tipos de datos es la naturaleza de información que
almacenan: mientras que el resto de los tipos de datos almacenan información relativa a los
cálculos del programa los punteros almacenan direcciones de memoria; esto es, hacen
referencia a la zona de memoria en donde se encuentran los datos.
Programación 17/92
La forma de definir un puntero es la siguiente:
tipo_base * variable;
y para referirse a la dirección de memoria donde está almacenada una variable se emplea el
operador "&". Veamos un ejemplo:
int a=7; int *pa; //puntero a dato de tipo entero pa=&a; //almacenamos en el puntero la dirección de memoria de a
Es posible acceder al contenido de la dirección que almacena el puntero. En el caso
anterior *pa es el valor de tipo int 7. pa es de tipo int * y su valor es el de una posición de
memoria que contiene el valor 7. En este caso, el * se llama operador indirección y opera
sólo sobre una variable puntero. Veamos un ejemplo:
/* *ejemplo6_3.c */ #include <stdio.h> main() { int u=3; int v; int *pu; int *pv; pu=&u; v=*pu; pv=&v; printf("\n u=%d &u=%X pu=%X *pu=%d",u,&u,pu,*pu); printf("\n v=%d &v=%X pv=%X *pv=%d",v,&v,pv,*pv); }
un posible resultado de la ejecución de este código es:
u=3 &u=F8E pu=F8E *pu=3 v=3 %v=F8C pv=F8C *pv=3
El operador & se llama operador dirección y sólo puede operar sobre operandos que
tengan una dirección única asignada, como por ejemplo sobre variables ordinarias.
Veamos un ejemplo:
Programación 18/92
/* *ejemplo6_4.c */ #include <stdio.h> main() { int v=3; int *pv; pv=&v; printf("\n *pv=%d v=%d ",*pv,v); *pv=0; printf("\n*pv=%d v=%d",*pv,v); system("pause"); }
la salida de este programa será:
*pv=3 v=3 *pv=0 v=0
Recordemos cómo los punteros utilizaban para pasar variables por referencia una función,
para que ésta pueda modificar el valor de la variable de la función origen:
/* *ejemplo6_5.c */ #include <stdio.h> void func1(int u,int v) { u=0; v=0; printf("\nDentro de func1: u=%d,v=%d",u,v); } void func2(int *pu, int *pv) { *pu=0; *pv=0; printf("\nDentro de func2: *pu=%d,*pv=%d",*pu,*pv); } Main5() { int u=1; int v=3; printf("\n Antes de la llamada a func1: u=%d v=%d",u,v); func1(u,v); printf("\n Despues de la llamada a func1: u=%d v=%d",u,v); func2(&u,&v);
Programación 19/92
printf("\n Despues de la llamada a func2: u=%d v=%d",u,v); system("pause"); }
La salida de este programa será:
Antes de la llamada a func1: u=1 v=3 Dentro de func1: u=0 v=0 Despues de la llamada a func1: u=1 v=3 Dentro de func2: *pu=0 *pv=0 Despues de la llamada a func2: u=0 v=0
3.1 Aritmética de punteros
Los punteros no permiten sólo almacenar direcciones de memoria o modificar el contenido
de dichas direcciones, también permiten incrementar el valor de las variables puntero para
acceder a las direcciones de memoria contiguas a la dirección original. La aritmética de
punteros permite que se realicen accesos a elementos del tipo base almacenados en
direcciones contiguas con tan sólo incrementar las direcciones de memoria almacenadas en
los punteros.
De un modo más formal, la aritmética de punteros se basa en la posibilidad de añadir o
sustraer un valor de tipo entero a una variable de tipo puntero, obteniendo así una
referencia a los elementos del tipo base situados en las direcciones de memoria anteriores o
posteriores a la almacenada originalmente en el puntero.
Programación 20/92
Es importante destacar que sólo es posible hacer referencia a elementos completos del tipo
base. Por tanto, cuando se suma o resta una unidad a una variable puntero lo que realmente
se hace es decrementar o incrementar la correspondiente dirección de memoria una
cantidad igual al tamaño en bytes del tipo base.
3.2 Punteros NULL y void
Cuando se definen los punteros, si no se inicializan, contienen valores aleatorios y el
resultado de intentar acceder a la posición de memoria a la que apuntan puede tener
resultados completamente impredecibles. Una forma de minimizar los posibles daños es
asegurarse que todos los punteros que no hayan recibido un valor útil contienen el valor
más inocuo posible; este valor es NULL (constante definida dentro de varios ficheros
cabecera, entre ellos stdio.h), dirección de memoria que se corresponde con el valor 0. Por
convenio, es imposible que esta dirección de memoria contenga algún dato válido
relacionado con el programa y cualquier compilador está preparado para que un acceso a
esta posición de memoria genere un error de ejecución y termine el programa en curso.
int * puntero= NULL;
Otro motivo para emplear estos punteros nulos es que así es posible distinguir entre un
puntero que ya ha sido inicializado y otro que no.
Un puntero void es un puntero genérico que puede apuntar o cualquier tipo de dato:
void *p; int i; float f; ... p = &i; p = &f;
La función de estos punteros es simplificar la conversión entre punteros de distintos tipos
base: estos punteros permiten convertir cualquier otro puntero a, o desde, un puntero a void
sin que el compilador genere ningún aviso ni informe de ningún error. Veámoslo con un
código de ejemplo:
Programación 21/92
/* *ejemplo6_6.c */ #include <stdio.h> int main(void) { long Dato = 0x41414141; long * refLong; short * refShort; void * refVoid; refLong = &Dato; refVoid = refLong; refShort = refVoid; printf("La dirección almacenada en refLong es = %p\n",refLong); printf("El dato referenciado por refLong es = 0x%lx\n\n",*refLong); printf("La dirección almacenada en refShort es = %p\n",refShort); printf("El dato referenciado por refShort es = %x\n",*refShort); printf("La dirección almacenada en refVoid es = %p\n",refVoid);
system("pause"); }
3.3 Asignación dinámica de memoria
La memoria dinámica es una zona de memoria perteneciente al programa diferente de la
memoria de datos de dicho programa. La memoria de datos es la que se encarga de
almacenar todas las variables correspondientes al programa, pudiéndose distinguir en ella
dos partes: la destinada al almacenamiento de las variables globales y la destinada al
almacenamiento de las locales. La memoria dinámica delimita una zona memoria donde la
reserva no se realiza definiendo variables, sino por medio de funciones específicas de
reserva y medición de memoria. Esta reserva de memoria se realiza en tiempo de ejecución
y no en tiempo de compilación, de ahí su nombre.
Las dos funciones que nos permiten reservar memoria son:
void* malloc (int cantidad_memoria); void* calloc (int número_elementos, int tamaño_de_cada_elemento);
Programación 22/92
Estas dos funciones (incluidas en el fichero <stdlib.h>) reservan la memoria especificada y
devuelven un puntero a la zona en cuestión. Si no se ha podido reservar el tamaño de
memoria especificado devuelve un puntero con el valor NULL. El tipo del puntero
devuelto es void, es decir, un puntero a cualquier cosa. Por tanto, tras reservar la
memoria suele realizarse una operación cast (de conversión de tipo).
Dado que en C un mismo tipo de dato puede ocupar distintos tamaños dependiendo del
compilador, es muy habitual emplear el operador sizeof en la reserva de memoria. Veamos
un ejemplo
/* *ejemplo6_7.c */ #include <stdlib.h> #include <stdio.h> main() { int *p_int; float *mat; p_int = (int *) malloc(sizeof(int)); mat = (float *)calloc(20,sizeof(float)); if ((p_int==NULL)||(mat==NULL)) { printf ("\nNo hay memoria"); exit(1); } /* Aquí iría el código que libera la memoria */ }
Este programa declara dos variables que son punteros a entero y a float. A estos
punteros se les asigna una zona de memoria, para el primero se reserva memoria para
almacenar una variable entera y en el segundo se crea una matriz de 20 elementos, cada
uno de ellos un float. Obsérvese el uso de los operadores cast para modificar el tipo del
puntero devuelto por malloc y calloc, así como la utilización del operador sizeof.
3.3.1 Liberación de la memoria.
Una vez que hemos reservado memoria dinámicamente esta permanecerá ocupada hasta
que nosotros indiquemos que esa memoria no volverá a ser necesaria y puede ser liberada.
Programación 23/92
La función que nos permite liberar la memoria asignada con malloc y calloc es
free(puntero), donde puntero es el puntero devuelto por malloc o calloc.
Ahora, en el ejemplo anterior, podemos escribir el código etiquetado como : /* Ahora
iría el código que libera la memoria*/
free (p_int); free(mat);
3.3.2 Función realloc
Permite ampliar un bloque de memoria que fue anteriormente asignado. Recibe dos
argumentos: el puntero al bloque original y el tamaño total en bytes del nuevo bloque.
Veamos un ejemplo:
int *p; p=(int *)malloc (10*sizeof(int)); /* usamos malloc para pedir espacio para guardar 10 enteros */ ........ /* nos damos cuenta de que necesitamos más espacio */ p=(int *) realloc(p, 20*sizeof(int));
Sino hay suficiente espacio en memoria la función devolverá NULL y dejará la estructura
inicial inalterada. Puede suceder que realloc encuentre memoria libre pero en una posición
distinta de memoria de la que estaba el bloque original. En tal caso, realloc copia todos los
datos al nuevo bloque.
A continuación presentamos un programa que reordena una lista de números introducidos
por teclado usando punteros
/* *ejemplo6_8.c */ #include <stdio.h> void reordenar(int n, int *x) { int i,elem,temp; for (elem=0;elem< n-1;++elem)
Programación 24/92
for (i=elem +1 ; i <n;++i) if (*(x+i) < *(x+elem) ) /* intercambiamos los elementos */ { temp=*(x+elem); *(x+elem) = *(x+i); *(x+i)=temp; } } main() { int i,n,*x; printf("¿Cuantos numeros seran introducidos?"); scanf("%d",&n); x=(int *) malloc(n*sizeof(int)); for (i=0;i<n;i++) { printf("\nDame el numero en la posicion %d:",i); scanf("%d",x+i); } reordenar(n,x); printf("\nLista reordenada:"); for(i=0;i<n;++i) { printf("\nPosicion %d, valor %d",i,*(x+i)); } free (x); system("pause"); }
3.4 Punteros constantes vs punteros a constantes
Un puntero constante es un puntero que no se puede cambiar, pero que los datos apuntados
por el puntero pueden ser cambiados. Se define del siguiente modo:
<tipo dato> *const <nombre puntero>=<direccion de variable>;
Por ejemplo,
int x,y; int *const p1=&x;
A partir de aquí no se puede modificar el valor de p1 pero si el de su contenido (*p1). No
es posible hacer p1=&y pero sí se puede cambiar el contenido de la dirección de memoria
a la que apunta el puntero; esto es, se puede hacer *p1=y.
Programación 25/92
En los punteros a constantes lo que podemos modificar es el puntero pero no el contenido
del mismo. Estos punteros se definen del siguiente modo:
const <tipo dato> *<nombre puntero>=<direccion de constante>;
por ejemplo
const int x=25; const int y=50; const int *p1 = &x;
No se puede cambiar el valor al que apunta el puntero: *p1=14; pero sí se puede cambiar la
dirección de memoria la que apunta: p1=&y.
También pueden definir punteros constantes a constantes:
const int x=20; const int *const p1 = &x;
Como regla general:
• Si se sabe que un puntero siempre apuntará a la misma posición y nunca necesita
ser reubicado, defínelo como un puntero constante.
• Si se sabe que el dato apuntado por el puntero nunca necesitará cambiar, defínelo
como un puntero a una constante.
Los punteros a constantes son útiles para conseguir un efecto similar al paso por valor en el
caso de los array y las cadenas de caracteres. Estas variables son punteros en sí y, por
tanto, se pasan siempre por referencia y pueden ser modificadas por la función receptora.
Si en la función receptora definimos el puntero como un puntero a una constante la función
no podrá modificar el contenido del puntero y habremos conseguido un efecto similar al
paso por valor.
Programación 26/92
3.5 Punteros de funciones a otras funciones
Cuando se define una función T () el compilador asigna una dirección de memoria donde
se situará el código de la función f. Se puede definir un puntero que apunta a una función
que devuelve T de la siguiente forma:
T (*p) ();
Esta sentencia define un puntero una función, es decir, una variable que almacena una
dirección de memoria que se corresponde con la posición donde se encuentra el código de
una función que devuelve un dato de tipo T. Así por ejemplo la siguiente definición:
int (*p) (int a, int b);
define un puntero a una función que devuelve un valor entero y acepta dos argumentos
enteros.
Un puntero a una función puede ser pasado como otro argumento a otra función. Esto
permite que una función sea transferida a otra, como si la primera función fuera una
variable. La función que es enviada como un argumento se llama huésped y la otra se
llama anfitriona. Llamadas sucesivas a la función anfitriona pueden pasar diferentes
punteros (esto es, diferentes funciones huésped) a la anfitriona. Mostramos cómo funciona
esto mediante un ejemplo:
/* *ejemplo6_9.c */ #include <stdio.h> int huesped1(int a,int b) { return(a+b); } int huesped2(int a,int b) { return (a-b); } void procesar(int (*pf)(int a, int b)) {
Programación 27/92
int j=10,k=5,rdo; rdo=(*pf)(j,k); printf("Resultado:%d\n",rdo); } main() { procesar (huesped1); procesar (huesped2); system("pause"); }
la salida de este programa será:
Resultado:15 Resultado:5
4 Arrays
Un array, vector o matriz es una colección de variables del mismo tipo, que tienen un
nombre común y están dispuestos en posiciones consecutivas de la memoria del ordenador.
Cada elemento del array se distingue de los demás por su índice (número de orden dentro
del array). Un array se caracteriza por su tipo base (el tipo de cada elemento), el número de
dimensiones del mismo y la longitud en cada dimensión.
Por lo tanto, para declarar un array se han de indicar dichas características en el siguiente
orden: tipo y nombre, como el resto de las variables, y además el número de elementos en
cada dimensión. Para ello, se coloca el tamaño de cada dimensión entre corchetes:
tipo nombre [tamaño1][tamaño2]...[tamañoN];
Por ejemplo, las siguientes declaraciones son para un array de 10 enteros y una matriz de
3x9 reales:
int MiVector[10]; float MiMatriz[3][9];
Esta sentencia
simplemente reserva
Programación 28/92
espacio de memoria para la cantidad de datos indicada; en el caso de MiVector se reservó
espacio para almacenar datos de tipo entero; sin embargo no se inicializa cada una de esas
posiciones de memoria que contendrá los datos. Esta inicialización debe realizarse
manualmente; para ello debemos acceder a cada posición del array e indicar qué valor debe
contener. Cada elemento
del array se identifica por el
nombre de dicho array
seguido del índice
correspondiente al elemento
entre corchetes; el índice
correspondiente al primer
elemento es siempre el 0.
De la misma forma, el
elemento de la esquina superior izquierda de la matriz MiMatriz sería MiMatriz[0][0]. Así,
por ejemplo, MiVector[2] accederá a la tercera posición del array MiVector y la sentencia
MiVector[2]=34; inicializa a 34 la tercera posición del array.
Lo más interesante de los arrays es la posibilidad de emplear una variable para indicar el
elemento al que queremos hacer referencia. De esta forma, modificando el valor de dicha
variable (por ejemplo, mediante un bucle), puede realizarse una operación con todo el
array, sin necesidad de repetirla para cada elemento individual; por ejemplo, las líneas
siguientes dividen cada elemento del array por e elevado a i:
for (i=0;i<10;i++) MiVector[i] = MiVector[i]/exp(i);
El tamaño de un array se fija al escribir el programa y no se puede variar durante su
ejecución. Por ello, las dimensiones del array se fijan al definirlo. Se puede usar una
constante numérica, como en el ejemplo visto, o bien una constante simbólica (creada con
#define) como a continuación.
#define MAXELEM 30 int A[MAXELEM]; float resultados[MAXELEM][2];
Programación 29/92
Utilizar constantes simbólicas tiene la ventaja de que si se utiliza la constante en la
definición de otros arrays y/o como límite de bucles, se puede cambiar dicho número para
todo el programa modificándolo una sola vez, en el #define.
Hasta C99 no se podían utilizar variables para definir el tamaño de un array, aunque
estuviesen inicializadas y no se modifique su valor. Por tanto, no sería válida la siguiente
declaración:
int N=5; int Vec1[N];
El que los arrays tengan un tamaño fijo no quiere decir que haya que utilizar siempre dicho
tamaño al operar con ellos: no es posible utilizar más elementos de los que tiene el array,
pero sí es posible utilizar menos. Por ello, en ocasiones se declara un array fijo, de un
tamaño suficientemente grande para contener el máximo número posible de elementos, y
después se utiliza sólo una parte del mismo, que puede variar según sea necesario.
Salvo para un tipo especial de arrays (las cadenas de caracteres, que abordaremos más
tarde en este tema), no existe una instrucción para leer o escribir un array completo. Por
tanto, la forma de hacerlo es leer o escribir sus elementos individualmente, utilizando un
bucle si es necesario.
El compilador de C no comprueba los límites de los arrays. Es decir, si hemos declarado
un array como
int V[20];
cuyos elementos van del V[0] al V[19], el compilador no protesta si durante el programa
utilizamos el elemento V[20] o el V[300], que no existen. Es responsabilidad del
programador el no sobrepasar los límites del array. Es peligroso que suceda esto, porque al
utilizar un elemento fuera de los límites del array estamos utilizando una posición de
memoria que probablemente pertenece a otra variable, lo que puede hacer que el programa
dé resultados absurdos o que pierda el control.
Programación 30/92
4.1 Inicialización de Arrays
Los arrays pueden inicializarse al declararlos, al igual que las variables corrientes. Para
ello, se ponen los valores iniciales entre llaves, separados por comas, como en el ejemplo
siguiente:
int A[5]={1,2,3,-4,-5}; char color[4]={'R','O','J','O'};
Si se ponen menos valores de los que caben en el array, los valores dados se asignan a las
primeras posiciones del array y el resto se inicializa la cero. Si se ponen más valores de los
que caben en el array, se produce un error. Para evitar tener que contar los valores
introducidos y poner este valor en la declaración del array, puede dejarse este dato en
blanco, y el compilador se encarga de calcularlo. Por ejemplo:
int B[]={1,1,0,0,2,2};
El compilador asumiría el array B como de 6 posiciones. En el caso de un array de más de
1 dimensión, hay que poner todas las dimensiones salvo la primera (si no, el compilador no
sabría si una matriz inicializada con 12 elementos es de 6x2, 3x4, 4x3 o 2x6) :
int C[][2]={-1,-2,-3,-4,-5};
El compilador asumiría el array C como de 3 x 2, con el último elemento - el C[2][1] - con
valor 0.
4.2 Punteros y arrays unidimensionales
El nombre de un array es realmente un puntero al primer elemento del array.
Por tanto, si x es un array unidimensional la dirección al primer elemento de la
formación se puede expresar tanto como &x[0] o simplemente como x. La dirección del
segundo elemento será &x[1] o (x+1) y así sucesivamente.
Programación 31/92
El número de celdas de memoria asociadas con un elemento del array puede depender del
compilador. Sin embargo, cuando se escribe la dirección de un elemento de la formación
de la forma (x+i) el programador no tiene que preocuparse por el número de celdas de
memoria asociadas con cada tipo de datos del array. El compilador lo ajusta
automáticamente.
Como &x[i] y (x+i) representan la dirección del i-ésimo elemento de x, el contenido de
esta dirección de memoria puede referenciarse mediante x[i] o mediante *(x+i).
Un array es un puntero constante. Por tanto, no es posible asignar una dirección arbitraria a
un nombre de un array o a un elemento del mismo. Por tanto, expresiones como x, (x+i) y
&x[i] no pueden aparecer en la parte izquierda de una expresión de asignación. Tampoco
podemos hacer algo como x++.
A continuación presentamos un programa que, empleando punteros, pide 4 datos al usuario
y calcula la media.
/* *ejemplo6_10.c */ #include <stdio.h> #define NUMERO_DATOS 4 int main(void)
Programación 32/92
{ float datos[NUMERO_DATOS]; float media= 0.0; float *ptr; short leidos; int j; ptr = datos; for(j = 0; j < NUMERO_DATOS; j++) { printf("Introduzca el dato número %d: ", j+1); leidos = scanf("%f", ptr); ptr ++; } for(j = NUMERO_DATOS; j >0; j--) { ptr--; media= media+ *ptr; } media= media/ NUMERO_DATOS; printf("La media es %f\n", media); system("pause"); }
4.3 Programas de ejemplo
Para afianzar lo visto hasta ahora o haremos un pequeño programa que calcule la media de
un conjunto de números, y después determine cuántos son mayores que la media, cuántos
iguales y cuántos menores. Es necesario almacenar los números, ya que la media sólo se
sabe después de haberlos leído todos. Por ello, hay que limitar la cantidad de números a
leer al tamaño del array. Lo que sí es posible es que se lean menos, por lo que al calcular la
media, se deben tener en cuenta los leídos realmente, y no el tamaño del array.
/* *ejemplo6_11.c */ #include <stdio.h> #define MAXNUM 20 main() { float num[MAXNUM]; float suma, media; int i,k,mayores,iguales,menores; printf("\n\nDame los numeros a promediar (Max. %d. "
Programación 33/92
"0 para finalizar) : ", MAXNUM); for ( i=0 ; i<MAXNUM ; i++ ) { scanf("%f",&num[i]); if (num[i]==0) break; suma = suma + num[i]; } if (i>0) { media=suma/i; printf("\nLa media de los %d numeros es %f \n",i,media); for (k=0;k<i;k++) if (num[k]>media) mayores++; else if (num[k]==media) iguales++; else menores++; printf("Hay %d numeros mayores, %d iguales y %d menores\n", mayores, iguales, menores); } system("pause"); }
Este es un ejemplo de programa en el que se declara un array con un tamaño fijo, pero se
utiliza una parte variable del mismo, pudiendo dejar sin utilizar las últimas posiciones del
array. El cálculo de la media se coloca dentro del if para evitar una posible división por 0.
A continuación mostraremos otro ejemplo en el que se realiza una suma de matrices. Una
de las matrices se generará con números aleatorios, mientras que la otra se introducirá por
teclado. Para operar (leer, escribir, sumar,....) con todos los elementos de una matriz, lo
más fácil es utilizar dos bucles anidados, el exterior para recorrer las filas y el interior para
las columnas (para recorrer la matriz por filas, si se quiere recorrer por columnas, el orden
es el contrario):
/* *ejemplo6_12.c */ #include <stdio.h> #include <stdlib.h> #include <time.h> #define FIL 5 #define COL 7 int A[FIL][COL],B[FIL][COL],C[FIL][COL]; int fila,columna; main() { system("cls"); srand(time(NULL));
Programación 34/92
for (fila=0;fila<FIL;fila++) for (columna=0;columna<COL;columna++) { A[fila][columna]=rand()%100; printf("\nDame el elemento B(%d,%d) ",fila,columna); scanf("%d",&B[fila][columna]); } for (fila=0;fila<FIL;fila++) /* suma */ for (columna=0;columna<COL;columna++) C[fila][columna]=A[fila][columna]+B[fila][columna]; system("cls"); printf("Matriz A\n\n"); for (fila=0;fila<FIL;fila++) { for (columna=0;columna<COL;columna++) printf("%4d",A[fila][columna]); printf("\n"); } printf("\nMatriz B\n\n"); for (fila=0;fila<FIL;fila++) { for (columna=0;columna<COL;columna++) printf("%4d",B[fila][columna]); printf("\n"); } printf("\nMatriz C\n\n"); for (fila=0;fila<FIL;fila++) { for (columna=0;columna<COL;columna++) printf("%4d",C[fila][columna]); printf("\n"); } system("pause"); }
El primer par de bucles inicializa la matriz A con números aleatorios entre 0 y 99 (esto se
consigue mediante la función rand (); y al mismo tiempo permite leer la B por teclado. El
segundo par realiza la suma C=A+B. Los restantes son para la presentación de las matrices
en pantalla. Al escribir las matrices, se usa el especificador %4d para que escriba los
números ocupando siempre 4 posiciones, de forma que queden espaciados de forma
regular. Al terminar cada fila, se salta de línea.
4.4 Arrays y reserva dinámica de memoria
Dado que un nombre de array es realmente un puntero al primer elemento es posible
definir la formación como un variable puntero en vez de como una variable convencional.
Sintácticamente las dos definiciones son equivalentes:
Programación 35/92
int *x int x[10];
Sin embargo la declaración segunda produce la reserva de un bloque fijo de memoria al
principio de la ejecución del programa, mientras que esto no ocurre si la formación se
define en términos de punteros (primera declaración). Por tanto, el uso de una variable
puntero para representar un array requiere asignarle memoria antes de que los elementos
de la formación sean procesados. Para ello, se utiliza la función malloc. Si hacemos una
declaración como puntero:
int *x;
Ahora tendremos que asignar memoria para los elementos del array:
x=(int *) malloc (10* sizeof(int));
El cast que precede a malloc (int * entre paréntesis) es necesario porque malloc devuelve
un puntero genérico y es necesario especificarle a que tipo de datos va a apuntar.
La instrucción anterior ha reservado memoria para poder almacenar 10 enteros.
Si queremos inicializar el array con una serie de valores iniciales no podemos declararlo
como puntero:
int x[]={1,2,3,4,5,6,7,8,9,10};
Una vez que hemos hecho uso del array reservado dinámicamente debemos liberar su
memoria:
free (x);
A continuación presentamos un código que reserva espacio dinámicamente para un array y
opera con él empleando punteros.
/* *ejemplo6_13.c */ #include <stdio.h> #include <stdlib.h> int main() { int *pa;
Programación 36/92
int n, i; printf(" Tamaño del array:"); scanf("%d", &n); if ((pa = (int *)malloc(n*sizeof(pa))) == NULL ) { printf(" No hay memoria\n"); exit(-1); } for ( i = 0; i < n; i++ ) *(pa+ i) = 2*i; puts("\n\n Contenido del array:\n"); for ( i = 0; i < n; i++ ) printf("%d\t", *(pa+ i)); free(pa); system("pause"); }
4.5 Punteros y arrays multidimensionales
Un array bidimensional no es más que una colección de arrays unidimensionales. Por
tanto, se puede definir un array bidimensional como un array de punteros:
int *ptr[10];
Por ejemplo, podemos hacer cosas del tipo:
int v=5, k=9; ptr[0]=&v; ptr[1]=&k;
También:
char *nombres_meses[12]={“Enero”, “Febrero”, “Marzo”, ….}
Si hacemos char *nombres[10]; el sistema nos reserva memoria para almacenar 10
punteros pero cada puntero está vacío, hay que reservar memoria para cada uno
individualmente:
nombres[i]=(char *) malloc (12*sizeof(char));
También puede definirse un array multidimensional a partir de un puntero a puntero:
char** nombres. En este caso ni siquiera tenemos memoria para almacenar los punteros.
Por tanto, primero habrá que reservar espacio para los punteros:
Programación 37/92
nombres=(char **) malloc (12*sizeof(char *)); /* memoria para 12 punteros a char */
y a continuación habrá que reservar espacio para los datos que serán apuntados por los
punteros:
nombres[i]=(char *) malloc (12*sizeof(char)); /* memoria para cada puntero individual */
Por ejemplo:
char **pp; pp=(char **)malloc(10*sizeof(char *)); pp[0]=(char *) malloc(15*sizeof(char)); strcpy(pp[0],"pepito"); printf("%c\n",pp[0][1]); /* escribe la e */
Frente a hacer:
char pp2[10][15]; strcpy(pp2[0],"santiago"); printf("%c\n",pp2[0][2]); /* escribe la letra n */
Una vez hayamos acabado de usar la matriz debemos liberar la memoria reservada.
Primero habrá que liberar la de cada puntero individual:
free (nombres[i]); /* memoria para cada puntero individual */
y a continuación la memoria del array de punteros
free (nombres);
A continuación mostramos un código en el cual se multiplican dos matrices cuadradas
cuya memoria se reserva dinámicamente.
/* *ejemplo6_14.c */ #include <stdlib.h> #include <stdio.h> float **reservarMemoria(int fil, int col)
Programación 38/92
{ float **m; int j; m = (float **) malloc(fil * sizeof(float *)); for (j = 0; j < fil; j++) m[j] = (float *)malloc(col * sizeof(float)); return (m); } void leerMatriz(float **m, int fil, int col) { int i, j; float dato; for (i = 0; i < fil; i++) { for (j = 0; j < col; j++) { printf("Elemento [%d,%d]: ", i,j); scanf("%f", &dato); m[i][j]=dato; } printf("\n"); } } void imprimirMatriz(float **m, int fil, int col) { int i, j; for (i = 0; i < fil; i++) { for (j = 0; j < col; j++) { printf("[ %e ] \t", m[i][j]); } printf("\n"); } } void multiplicarMatriz(float **A, float **B, float **C, int dim) { int i, j, k; for(i = 0; i < dim; i++) { for(j = 0; j < dim; j++) { C[i][j] = 0.0; for(k = 0; k < dim; k++) { C[i][j] += A[i][k] * A[k][j]; }
Programación 39/92
} } } void liberarMemoria(float **m, int fil, int col) { int j; for (j = 0; j < fil; j++) free(m[j]); free(m); return; } int main(void) { float **A, **B, **C; int dim; printf("Número de filas: "); scanf("%d", &dim); A = reservarMemoria(dim, dim); B = reservarMemoria(dim, dim); C = reservarMemoria(dim, dim); system ("cls"); printf("Introduzca los datos de la primera matriz: "); leerMatriz(A,dim, dim); system ("cls"); printf("Introduzca los datos de la segunda matriz"); leerMatriz(B,dim, dim); multiplicarMatriz(A, B, C, dim); imprimirMatriz(C, dim, dim); liberarMemoria(A, dim, dim); liberarMemoria(B, dim, dim); liberarMemoria(C, dim, dim); system("pause"); }
5 Cadenas de caracteres
Una cadena de caracteres o string es un array que cumple dos condiciones:
• Sus elementos son de tipo char (caracteres)
• Uno de ellos es ‘\0’ (carácter de código ASCII 0)
Programación 40/92
El carácter ‘\0’ sirve para marcar el final de la cadena. Puede ir en cualquier parte del
array. El carácter ‘\0’ y los que le siguen no se tienen en cuenta al operar con la cadena.
No hay que confundir el ‘\0’ de fin de cadena, que es el carácter nulo, de código ASCII 0,
con el carácter '0', cuyo código ASCII es el 48 (30 en hexadecimal). A la hora de leer una
cadena con scanf no hay que preocuparse de si hay un cero final o no, después de la lectura
cuando la función añade un cero después del último carácter leido.
Para este tipo especial de arrays, el lenguaje C proporciona un conjunto de funciones,
definidas en string.h, que permiten, entre otras cosas:
• Leer o escribir una cadena (un array completo).
• Averiguar los caracteres que contiene una cadena (antes del \0).
• Buscar un carácter o un grupo de caracteres dentro de una cadena.
• Unir dos cadenas, o extraer una parte de una cadena.
Todas estas funciones sólo sirven para cadenas (arrays de caracteres), no para arrays de
otro tipo de datos.
5.1 Declaración de una cadena
Las cadenas se declaran como un array de tipo char, de una sola dimensión. Por ejemplo:
char micadena[40];
El ejemplo anterior declara una cadena de 40 posiciones. Como hay que dejar sitio para el
caracter \0, quedan utilizables 39 posiciones. Debemos tener siempre en cuenta ésto a la
hora de reservar espacio para una cadena de caracteres.
5.2 Inicialización de cadenas
Al declarar una cadena se le puede dar un valor inicial de la siguiente forma:
char autor[37]="Albino Blanco Canosa - E.P.S. Ferrol";
Programación 41/92
La longitud del mensaje es de 36 caracteres, por lo que se declara de 37 para que quepa el
\0, que mete el compilador automáticamente. Si se quiere evitar tener que contar los
caracteres de la cadena para darle la longitud, puede dejarse en blanco la longitud, y el
compilador le dará automáticamente la necesaria para que quepa el mensaje y el \0 final:
char autor[]="Albino Blanco Canosa - E.P.S. Ferrol";
No pasa nada si el array se declara más largo de lo necesario, simplemente quedarán
posiciones sin usar después del carácter \0 (se gasta más memoria de la necesaria):
char autor[60]="Albino Blanco Canosa - E.P.S. Ferrol";
5.3 Lectura y escritura de cadenas
La lectura de una cadena completa puede hacerse mediante dos funciones: gets() y scanf().
Mediante gets(), se hace de la siguiente forma:
char micadena[20]; main() { gets(micadena); }
La función gets() lee caracteres de cualquier tipo, incluyendo espacios en blanco, hasta que
recibe un retorno de carro (tecla INTRO o ENTER). Entonces los almacena en la variable
que recibe como parámetro (micadena en el ejemplo anterior). No almacena el carácter de
retorno de carro, sino que lo substituye por el de fin de cadena (\0). La función gets()
cuando se teclea más caracteres de los que caben en la cadena, los lee y los almacena en
memoria, desbordando la cadena receptora; es decir, los coloca en posiciones de memoria
que probablemente pertenecen a otras variables. Por ello es importante dar el tamaño
necesario a la variable receptora. Una forma de evitar este problema es leer la cadena
mediante la función scanf(), utilizando además un indicador de longitud de campo en el
especificador de formato. Las cadenas se leen con el especificador %s. La longitud de
campo se indica colocando dicho número entre el % y la s. Por ejemplo:
Programación 42/92
char micadena[20]; main() { scanf("%19s",micadena); }
Obsérvese que no se utiliza el carácter & antepuesto al nombre de la variable a leer. Eso se
debe a que el nombre de un array, sin especificar el subíndice, es ya un puntero a la
primera posición del mismo, por ello es innecesario el &.
La escritura de una cadena puede hacerse con la función puts():
char micadena[]="Una bonita cadena"; main() { puts(micadena); }
También puede utilizarse la función printf() con el modificador %s para imprimir un
string, pudiendo además imprimir a la vez otras variables:
char micadena[]="Soy un string"; float velocidad=55.3; main() { printf("Ahi van un string %s y un numero %f",micadena,velocidad); }
5.4 Funciones para trabajar con cadenas
Las cadenas pueden utilizarse como cualquier array, elemento a elemento. Cada elemento
se accede por medido de su subíndice exactamente igual que en cualquier otro vector.
Ejemplificaremos esto mediante un programa que cuenta los espacios en blanco de una
cadena:
/* *ejemplo6_15.c */ #include <stdio.h> char cadena[200]; int i,blancos; main()
Programación 43/92
{ printf("Teclea una cadena : "); gets(cadena); for (i=0;cadena[i]!=0;i++) { if (cadena[i]==' ') blancos++; } printf("La cadena tenia %d espacios en blanco",blancos); system("pause"); }
El final de la cadena se alcanza cuando cadena[i] es 0.
El compilador de C posee una serie de funciones para trabajar con cadenas de caracteres
que están definidas en el fichero string.h. Por ejemplo, la función strlen(), que calcula la
longitud de la cadena que se pasó como argumento:
/* *ejemplo6_16.c */ #include <stdio.h> #include <string.h> char micadena[100]; int l; main() { printf("Teclea una cadena"); gets(micadena); l=strlen(micadena); printf("Has tecleado %d caracteres",l); system("pause"); }
La función strcmp compara dos cadenas y devuelve 0 si son iguales, un número menor
que cero si la primera es menor que la segunda, y mayor que cero si la primera es mayor:
/* *ejemplo6_17.c */ #include <stdio.h> #include <string.h> char cad1[20],cad2[20]; main() { printf("Teclea dos palabras : "); scanf("%19s %19s",cad1,cad2);
Programación 44/92
if (0==strcmp(cad1,cad2)) printf("Son iguales\n"); else if (strcmp(cad1,cad2)<0) printf("La primera es menor\n"); else printf("La primera es mayor\n"); system("pause"); }
Las palabras se comparan por orden alfabético, carácter a carácter, como en el diccionario.
La diferencia estriba en que los caracteres se comparan según su código ASCII. De esta
forma, las mayúsculas van antes que las minúsculas, la letra ñ va después que las demás y
los otros símbolos tienen un orden que no se les atribuye en el diccionario; hay que
consultar una tabla ASCII para saberlo. La función stricmp realiza una comparación
similar, pero ignorando la diferencia entre mayúsculas y minúsculas. Las funciones
strupr(cadena) y strlwr(cadena) pasan una cadena a mayúsculas y a minúsculas,
respectivamente.
La función strchr (char* cadena, char carácter) devuelve un puntero a la primera
ocurrencia del carácter que se pasa como segundo argumento dentro de la cadena apuntada
por el puntero que se le pasa como primer argumento:
char cad1[20]= "hola"; char c = 'l'; char* p = strchr (cad1, c); // p está apuntando a (cad1+2)
La función strcpy(destino,origen) copia el contenido de la cadena origen en destino; no
sirve una simple asignación. Es incorrecto hacer:
char cad1[20],cad2[20]; main() { printf("Teclea una palabra : "); scanf("%19s",cad1); cad2=cad1; }
La función strcat(s1,s2) concatena la segunda cadena al final de la primera:
Programación 45/92
/* *ejemplo6_18.c */ #include <stdio.h> #include <string.h> char cad1[160],cad2[80]; main() { printf("Teclea dos cadenas : "); gets(cad1); gets(cad2); printf("La cadena 1 es %s\n",cad1); printf("La cadena 2 es %s\n",cad2); strcat(cad1,cad2); printf("Las cadenas pegadas quedan %s\n",cad1); system("pause"); }
5.5 Arrays de cadenas
Cuando sea necesario manejar un conjunto de cadenas de caracteres podemos definir un
vector que contenga dichas cadenas. A continuación mostraremos un ejemplo donde se
utiliza un array de cadenas para almacenar los nombres de los meses. El programa pide un
mes en letra y contesta los días que tiene:
/* *ejemplo6_19.c */ #include <stdio.h> #include <string.h> char meses[][15]={"enero","febrero","marzo", "abril","mayo","junio","julio", "agosto","septiembre","octubre", "noviembre","diciembre"}; int duracion[]={31,28,31,30,31,30,31,31,30,31,30,31}; char mes[15]; int i; main() { printf("Dame un mes (en letra) : "); gets(mes); for (i=0;i<12;i++) { if (stricmp(mes,meses[i])==0) break; } if (i==12) printf("Ese mes no existe \n"); else printf("Ese mes tiene %i dias\n",duracion[i]);
Programación 46/92
system("pause"); }
Como ya indicamos, la primera dimensión de un array de varias dimensiones que se
inicializa se puede dejar en blanco, y el compilador cuenta los componentes. La función
stricmp() me permite ignorar la diferencia entre mayúsculas y minúsculas. Obsérvese que
se ha puesto una longitud de 15 para las filas, cuando sería suficiente 11 (10 letras de
septiembre más el 0 final). El programa funciona igual, sólo gasta más memoria de la
necesaria.
5.6 Punteros a cadenas
También se puede definir una cadena a través de un puntero:
char *cptr=”C a su alcance”;
En el caso anterior el sistema reserva memoria automáticamente para la cadena. Podemos
recorrer una cadena de la siguiente forma:
char *p; char *cptr=”C a su alcance”; p=cptr; while(*p) printf(“%c”,*p++);
A continuación mostramos un ejemplo que pasa una cadena a mayúsculas usando
punteros:
Programación 47/92
/* *ejemplo6_20.c */ #include <stdio.h> main() { char *p; char *cptr="C a su alcance"; //cptr = malloc(20*sizeof(char)); gets(cptr); p=cptr; while(*p) { if ((*p >= 'a') && (*p <= 'z')) *p=*p-32; printf("%c",*p); p++; } system("pause"); }
A continuación presentamos un código que lee líneas de texto y reserva memoria según la
longitud de la línea leída. A continuación se calcula el número de vocales por línea.
/* *ejemplo6_21.c */ #include <stdio.h> #include <stdio.h> #define NUMERO_DATOS 20 #define N 5 void entrada(char *cd[]) { char B[121] ; int j,tam; printf("\n\t Escribe %d lineas de texto\n",N); for(j=0;j<N;j++) { gets(B); tam=(strlen(B)+1)*sizeof(char); cd[j]=(char *) malloc(tam); strcpy(cd[j],B); } }
Programación 48/92
int vocales(char * c) { int k=0,j; for (j=0; j < strlen(c);j++) switch(tolower(*(c+j))) { case 'a': case 'e': case 'i': case 'o': case 'u': k++; } return k; } void salida(char *cd[], int *v) { int j; puts("\n\tSalida de las lineas junto al numero de vocales"); for (j=0;j<N;j++) printf("%s : %2d\n",cd[j],v[j]); } int main() { char *cad[N]; int j,voc[N]; entrada(cad); for(j=0;j<N;j++) voc[j]=vocales(cad[j]); salida(cad,voc); return 0; }
6 Estructuras
Una estructura es un tipo de dato que permite empaquetar bajo un mismo nombre
elementos de distinto tipo de datos que se encuentran relacionados lógicamente. Cada
elemento se denomina miembro de la estructura. Una estructura permite modelar
Programación 49/92
entidades del mundo real donde cada uno de los miembros de la estructura normalmente
representa un atributo de la entidad que se modela.
La sintaxis para definir una estructura es la siguiente:
struct nombre { miembro1; miembro2; .............. miembrom; };
Los nombres de los miembros deben ser distintos dentro de la misma estructura pero
pueden ser iguales a nombres de variables dentro del programa o a nombres de miembros
de otras estructuras. No se puede asignar un tipo de almacenamiento a un miembro
individual de una estructura. Tampoco se puede inicializar un miembro dentro de la
declaración de la estructura.
Una estructura ocupa en memoria al menos la suma de lo que ocupan sus miembros a
(algunos compiladores utilizan algunos bytes más para guardar información adicional).
Podemos averiguar su tamaño mediante el operador sizeof. Los distintos miembros de una
estructura no tienen necesariamente que estar almacenados en posiciones consecutivas de
memoria. En ocasiones, para optimizar el acceso a los distintos miembros de la estructura,
el compilador deja bytes de relleno entre los miembros. Esto hace que la suma del tamaño
de cada uno de los miembros de una estructura no tenga necesariamente coincidir con el
tamaño de dicha estructura:
/* *ejemplo6_22.c */ #include <stdio.h> struct Datos { int entero; char caracter; double real; }; int main(void) {
Programación 50/92
printf("Tamaño tipos de datos\n"); printf("---------------------\n"); printf("sizeof(int) = %d\n", sizeof(int)); printf("sizeof(char) = %d\n", sizeof(char)); printf("sizeof(double) = %d\n", sizeof(double)); printf("sizeof(struct Datos) = %d\n", sizeof(struct Datos)); printf("\n"); system("pause"); }
una posible salida de este programa es:
Tamaño tipos de datos --------------------- sizeof(int) = 4 sizeof(char) = 1 sizeof(double) = 8 sizeof(struct Datos) = 16
Veamos un ejemplo de una estructura que pretende representar una cuenta bancaria:
struct cuenta{ int num_cuenta; char tipo_cuenta; char nombre[80]; float saldo; };
Para declarar dos variables antiguocliente y nuevocliente que sean del tipo struct cuenta se
emplea la siguiente sintaxis:
struct cuenta antiguocliente, nuevocliente;
Es posible realizar de modo simultáneo la definición y la declaración de variables:
struct cuenta{ int num_cuenta; char tipo_cuenta; char nombre[80]; float saldo; }antiguocliente, nuevocliente;
Programación 51/92
Si no se quiere utilizar el nombre de la estructura en el futuro se puede omitir:
struct { int no_cuenta; char tipo_cuenta; char nombre[80]; float saldo; }antiguocliente, nuevocliente;
Es importante resaltar que al definir así la estructura no se puede usar el nombre "struct
cuenta" ni para definir nuevas variables ni para declarar el tipo de parámetros de funciones.
Es posible que uno de los tipos de datos incluidos dentro de una estructura sea otra
estructura:
struct fecha{ int mes; int dia; int año; }; struct cuenta{ int no_cuenta; char tipo_cuenta; char nombre[80]; float saldo; struct fecha ultimopago; };
A los miembros de una variable de tipo estructura se les puede asignar valores iniciales.
Los valores iniciales deben aparecer en el orden que se les asignarán a los correspondientes
miembros de la estructura, encerrados por llaves y separados por comas:
struct cuenta cliente={12345,'R',"Jose Garcia",586.30,5,24,90};
Para acceder a un miembro de una estructura se emplea el nombre de la variable estructura
seguido de un "." y el nombre del miembro al cual queremos acceder.
variabledetipoestructura.miembro
Veamos un ejemplo:
struct fecha{ int mes; int dia;
Programación 52/92
int año; } struct cuenta{ int no_cuenta; char tipo_cuenta; char nombre[80]; float saldo; struct fecha ultimopago; }cliente; cliente.no_cuenta cliente.nombre cliente.saldo
El operador punto (.) pertenece al grupo de mayor precedencia, por lo que
++variable.miembro equivale a ++(variable.miembro). De la misma forma
&cliente.no_cuenta equivale a &(cliente.no_cuenta) y accede a la dirección de memoria
donde comienza el miembro no_cuenta.
Si el miembro es a su vez una estructura podrán aparecer expresiones como
variable.miembro.submiembro. Si el miembro es un array el acceso se realiza como:
variable.miembro[numero]. En el ejemplo anterior:
cliente.ultimopago.mes cliente.nombre[2] &cliente.nombre[2]
Si empleamos arrays de estructuras el acceso se realiza como sigue:
struct cuenta clientes[100]; clientes[13].no_cuenta clientes[13].saldo clientes[13].ultimopago.mes
La mayoría de las nuevas versiones de compiladores de C permiten asignar una estructura
completa a otra:
Programación 53/92
struct cuenta cuenta1,cuenta2; ...... cuenta1=cuenta2;
Con algunos compiladores antiguos había que asignar los miembros uno a uno.
Para pasar estructuras a funciones algunos compiladores antiguos obligaban a pasar un
puntero. Las nuevas versiones permiten pasar la estructura al completo a una función. Es
conveniente tener en cuenta que cuando se pasa una estructura por valor a una función
todos los campos de la estructura en deben de ser copiados para generar las variables sobre
las cuales opera la función. Si la estructura tiene un tamaño considerable esto puede
suponer una penalización para la eficiencia del programa. Cuando la estructura se pasa por
referencia lo único que hay que copiar en la llamada la función es la dirección de memoria
donde se almacena la estructura, con lo cual la llamada es muy eficiente. Es posible
emplear un pequeño truco al realizar llamadas a funciones cuyos argumentos o estructuras
se pasan por valor: pasar un puntero a dicha estructura, pero definir ese puntero como una
constante:
void funcion (const struct fecha *hoy)
de este modo si se intenta modificar el valor de la estructura el compilador generará un
error.
A continuación presentamos un programa que muestra el uso de las estructuras:
/* *ejemplo6_23.c */ #include <stdio.h> #include <stdlib.h> struct Estructura { char letra; long entero; float real; char string1[256]; char *string2; };
Programación 54/92
int main(void) { struct Estructura ejemplo1 = {'a', 23L, 4.5, "Hola", "Estructura"}; struct Estructura ejemplo2; struct Estructura ejemplo3; ejemplo2 = ejemplo1; ejemplo3.letra = 'b'; ejemplo3.entero = 2345L; ejemplo3.real = 234.678; strcpy(ejemplo3.string1, "Cadena de Estructura1"); ejemplo3.string2 = (char *) malloc(128 * sizeof(char)); strcpy(ejemplo3.string2, "Cadena de Estructura2"); printf("%c\n", ejemplo3.letra ); printf("%ld\n", ejemplo3.entero ); printf("%f\n", ejemplo3.real ); printf("%s\n", ejemplo3.string1 ); printf("%s\n", ejemplo3.string2 ); system("pause"); }
6.1 Tipos de datos definidos por el usuario
A menudo resulta útil crear nuevos tipos de datos para mejorar la legibilidad de los
programas. Para definir estos nuevos tipos de datos se usa la sentencia typedef cuya
sintaxis es:
typedef tipo nuevo-tipo;
Donde tipo es un "tipo" de dato existente y "nuevo-tipo" es el nombre del tipo que estamos
definiendo. Vemos unos ejemplos:
typedef int edad; edad varon,hembra; /* equivale a int varon,hembra; */ typedef float altura [100]; altura hombres, mujeres; /* equivale a float
hombres[100],mujeres[100] */
Uno de los usos más comunes de esta sentencia es dar un nombre significativo y más breve
a las estructuras:
Programación 55/92
typedef struct { int no_cuenta; char tipo_cuenta; char nombre[80]; float saldo; }registro; registro anteriorcliente, nuevocliente;
6.2 Estructuras y punteros
Podemos acceder a la dirección de comienzo de una estructura de la misma manera que
para cualquier otra variable: mediante &. También se puede declarar un puntero a una
estructura:
typedef struct { int no_cuenta; char tipo_cuenta; char nombre[80]; float saldo; }cuenta; cuenta cliente,*pc; pc=&cliente;
Cuando tenemos un puntero a una estructura para acceder a sus miembros:
(*pc).saldo
Existe una forma equivalente que ahorra escritura:
pc->saldo vs cliente.saldo pc->nombre[2] vs cliente.nombre[2] *(pc->nombre+2) vs *(cliente.nombre +2)
A continuación mostramos un programa que empleando punteros a estructuras determina
si dos coordenadas son iguales:
/* *ejemplo6_24.c */ #include <stdio.h> #include <stdlib.h> struct Coordenadas { float x;
Programación 56/92
float y; }; int main(void) { struct Coordenadas origen = {0.0, 0.0}; struct Coordenadas *puntoA, *puntoB; puntoA = &origen; printf("Las coordenadas del origen son: "); printf("(%f %f) \n", puntoA->x, puntoA->y); puntoA = (struct Coordenadas *) malloc(sizeof(struct Coordenadas)); puntoB = (struct Coordenadas *) malloc(sizeof(struct Coordenadas)); if ((puntoA == NULL) || (puntoB == NULL)) { printf("Error, no hay memoria disponible\n"); return(0); } printf("Introduzca la coordenada x del punto A: "); scanf("%f", &(puntoA->x)); printf("Introduzca la coordenada y del punto A: "); scanf("%f", &(puntoA->y)); printf("Introduzca la coordenada x del punto B: "); scanf("%f", &(puntoB->x)); printf("Introduzca la coordenada y del punto B: "); scanf("%f", &(puntoB->y)); if ((puntoA->x == puntoB->x) && (puntoA->y == puntoB->y)) printf("Los puntos A y B son iguales\n"); else printf("Los puntos A y B son distintos\n"); free (puntoA); free (puntoB);
system("pause"); }
A continuación presentamos otro programa donde se busca el registro de un cliente que
corresponda con un número de cuenta dado:
/* *ejemplo6_25.c */ #include <stdio.h> #define N 3
Programación 57/92
typedef struct{ char *nombre; int no_cuenta; char tipo_cuenta; float saldo; }registro; registro *buscar(registro tabla[],int ncuenta) { int cont; for (cont=0;cont < N;++cont) if (tabla[cont].no_cuenta == ncuenta) return(&tabla[cont]); return(NULL); } main() { registro cliente[N]={ {"Lazaro",3333,'A',33.33}, {"Jose",6666,'R',66.66}, {"Rafael",9999,'Z',999.99}}; int ncuenta; registro *pt; printf("\n Dame un numero de cuenta a buscar:"); scanf("%d",&ncuenta); pt=buscar(cliente,ncuenta); if (pt!=NULL) { printf("Nombre:%s\n",pt->nombre); printf("Num.cuenta:%d\n",pt->no_cuenta); printf("Tipo de cuenta:%c\n",pt->tipo_cuenta); printf("Saldo:%f\n",pt->saldo); } else printf("ERROR: numero de cuenta inexistente\n"); system("pause"); }
7 Uniones
Las uniones, al igual que las estructuras, contienen miembros cuyos tipos de datos pueden
ser diferentes. Sin embargo, los miembros que componen una unión comparten el mismo
Programación 58/92
área de almacenamiento. Al asignar un valor a un miembro estamos sobreescribiendo los
valores de otros miembros. Se suelen utilizar cuando en el programa sólo utilizamos
simultáneamente uno de los miembros de la unión, con el consiguiente ahorro de memoria
en programa.
La sintaxis para definir una unión es:
union nombre { miembro1; miembro2; .............. miembrom; };
Los nombres de los miembros deben ser distintos dentro de la misma union pero pueden
ser iguales a nombres de variables dentro del programa o a nombres de miembros de otras
estructuras o uniones. No se puede asignar un tipo de almacenamiento a un miembro
individual de una unión. Tampoco se puede inicializar un miembro dentro de la
declaración de la unión.
Una unión ocupa en memoria al menos lo que ocupa el miembro de mayor tamaño
(algunos compiladores utilizan algunos bytes más para guardar información adicional).
Al igual que para las estructuras, se puede definir la unión a la vez que se declaran
variables del tipo unión.
union datos { char codigo; int valor; float dividendo; char nombre[40]; }miunion1,miunion2;
A continuación mostramos un ejemplo del uso de las uniones:
/* *ejemplo6_26.c */ #include <stdio.h>
Programación 59/92
main() { union id{ char color; int talla; }; struct { char fabricante[20]; float coste; union id descripcion; }camisa,blusa; printf("%d \n",sizeof (union id)); camisa.descripcion.color='B'; printf("%c %d\n", camisa.descripcion.color, camisa.descripcion.talla); camisa.descripcion.talla=12; printf("%c %d\n", camisa.descripcion.color, camisa.descripcion.talla); system("pause"); }
la salida del programa será:
2 B -24713 @ 12
Una unión puede ser inicializada pero sólo con un valor para uno de sus miembros. En
general, el valor que se especifique se asignará al primer miembro dentro de la unión. Los
compiladores modernos permiten asignar una unión a otra.
8 Archivos de datos
El uso de la memoria principal implica un tiempo de acceso muy reducido a los datos, pero
también presenta una limitación en cuanto al espacio disponible y su falta de conservación
para un uso posterior. Las estructuras de datos que hemos visto (arrays, estructuras
dinámicas creadas mediante malloc, variables enteras, flotantes, etc.) se almacenan en
memoria principal durante la ejecución del programa y se liberan al finalizar su ejecución.
En concreto, las variables locales de una función se liberan cuando se acaba de ejecutar la
Programación 60/92
función (a no ser que sean static) y las globales se liberan cuando se termina la ejecución
de la función main.
Sin embargo, la gran mayoría de las aplicaciones necesitan disponer de un mecanismo que
permita guardar datos que podamos recuperar al volver a ejecutar el programa. Para ello,
debemos guardar datos en almacenamiento secundario (ej. discos) que guardan la
información aunque el ordenador esté apagado. En este tema veremos las estructuras y
funciones necesarias para leer/escribir datos en ficheros (también llamados archivos) desde
nuestros programas C. A diferencia de los datos almacenados en memoria:
• Los ficheros pueden existir independientemente de los programas y pueden
accederlos múltiples usuarios.
• Su capacidad de almacenamiento es muy superior a la de la memoria principal.
• El tiempo de acceso es mayor.
Los archivos de texto contienen caracteres consecutivos y la interpretación de estos
caracteres depende de la forma y propósito con que los escribamos. Podemos escribir
carácter a carácter, cadenas enteras, etc. Las funciones que se usan para este tipo de
archivos son muy similares a las usadas para escribir por pantalla (printf, scanf, gets,
putchar, etc.).
Los archivos binarios organizan los datos en bloques de bytes contiguos de información.
Estos bloques representan estructuras de datos más complejas como tipos de datos
primitivos o estructuras. Para leer correctamente el archivo debemos saber con qué se
corresponde cada conjunto de bytes.
8.1 Apertura y cierre de un archivo
Un programa antes de poder trabajar con un fichero debe establecer un área de buffer
(bloque de memoria principal) donde se almacena la información temporalmente mientras
se transfiere información entre la memoria principal y la secundaria. En algunos textos se
utiliza también el nombre de flujo en lugar de área de buffer. Este área de información se
establece definiendo una variable de tipo FILE * . FILE es un tipo especial de estructura
Programación 61/92
manejada por el sistema. El programador debe simplemente definir un puntero a esta
estructura:
FILE *ptvar;
A la variable ptvar se le suele denominar manejador de fichero.
Existen tres variables estándar en el sistema que son del tipo FILE *: entrada estándar
(habitualmente el teclado), salida estándar (habitualmente la pantalla) y salida de errores
estándar (habitualmente la pantalla). El programador no tiene que definirlas pues ya están
definidas por defecto:
extern FILE *stdin; extern FILE *stdout; extern FILE *stderr;
Una vez hecho esto, debemos abrir el fichero. Esto significa asociar el área de buffer con
un fichero del disco. Para ello se utiliza la función de biblioteca fopen de la forma:
ptvar=fopen(nombre-archivo,tipo-apertura);
El nombre del archivo es una cadena de caracteres que representa el nombre del archivo en
el sistema de ficheros del sistema operativo en que trabajemos (esto es, el nombre del
archivo en el disco). Si no se incluye la ruta (path) completa al directorio en que está el
fichero se entiende que el archivo está en el mismo directorio que el programa que estamos
ejecutando. El tipo de apertura de archivo es una de las siguientes cadenas de caracteres:
• "r": abre un fichero de texto para lectura.
• "w": crea un fichero de texto para escribir; si el archivo ya existía lo trunca a
longitud cero.
• "a": abre o crea un fichero de texto para escribir al final.
• "rb": abre un fichero en modo binario para lectura.
• "wb": crea un fichero en modo binario para escribir; si el archivo ya existía lo
trunca a longitud cero.
• "ab": abre o crea un fichero en modo binario para escribir al final.
• "r+": abre un fichero de texto para lectura y escritura.
Programación 62/92
• "w+": crea un fichero de texto para lectura y escritura; si el archivo ya existía lo
trunca a longitud cero.
• "a+": abre o crea un fichero de texto para lectura y escritura posicionando el
apuntador al final.
• "r+b" ó "rb+": abre un fichero en modo binario para lectura y escritura.
• "w+b" ó "wb+": crea un fichero en modo binario para lectura y escritura; si el
archivo ya existía lo trunca a longitud cero.
• "a+b" ó "ab+": abre o crea un fichero en modo binario para lectura y escritura
posicionando el apuntador al final del fichero.
La función fopen devuelve un puntero al principio del área de buffer asociada con el
archivo. Devuelve un valor NULL si no se puede abrir el archivo (por ejemplo, si no se
puede encontrar un archivo que tratamos de abrir en modo "r"). Una vez abierto se pueden
leer/escribir (dependiendo del modo de apertura) datos del/al fichero.
Una vez que hemos finalizado de trabajar con el archivo hay que cerrarlo. Habitualmente,
el cierre de un fichero realiza el volcado de toda la información que pudiese quedar por
escribir en el fichero (porque todavía está en el buffer de entrada/salida). Sin embargo, la
especificación de C estándar no fuerza este comportamiento y es posible que haya
compiladores en los cuales al cerrar un archivo no se vuelque el contenido del bufer al
disco. La función fclose permite cerrar un fichero, y la función fflush vuelca el contenido
del bufer al fichero. En resumen, este es el código típico para trabajar con un fichero:
FILE *f; f=fopen("fichero.dat","r"); if (f == NULL) { printf("Error, el fichero llamado fichero.dat no existe"); exit(0); } ........ fflush (f); fclose(f);
Programación 63/92
8.2 Lectura y escritura de un archivo de texto
Un fichero se puede crear usando un procesador de texto o mediante un programa que abra
el fichero en modo escritura y le introduzca datos. El programa que presentamos a
continuación lee una línea de texto de la entrada estándar y la almacena en mayúsculas en
un fichero de datos:
/* *ejemplo6_27.c */ #include <stdio.h> #include <ctype.h> main() { FILE *f; char c; f=fopen("mifichero.dat","w"); do { c=getchar(); putc(toupper(c),f); } while (c != '\n'); fclose(f); }
La función putc (o fputc, que es totalmente equivalente a putc) es análoga a putchar, pero
necesita un argumento adicional que es el manejador de fichero (la variable f en el ejemplo
anterior). La función putc (fputc) devuelve como resultado el carácter escrito o la constante
del sistema EOF si el carácter no puede ser escrito.
La función getc (o fgetc, que es totalmente equivalente a getc) es similar a getchar pero
sólo necesita como argumento el manejador del fichero. La función getc (fgetc) devuelve
como resultado el carácter leído o la constante del sistema EOF en caso de que se llegue al
final del archivo.
char c; ....... putchar(c); /* escribimos en pantalla */ .......... c=getchar(); /* leemos de teclado */
Programación 64/92
......... putc(c,f); /* escribimos en el fichero manejado por f */ ...... c=getc(f); /* leemos un carácter del contenido del fichero */
Siempre que leemos o escribimos un carácter en un fichero la siguiente operación de
lectura/escritura se efectuará a continuación del que escribimos o leímos por última vez. El
sistema mantiene un apuntador a la posición del fichero donde se efectuará la siguiente
lectura/escritura. Si leemos/escribimos un carácter este apuntador se mueve una posición
para que la siguiente operación de lectura o escritura se efectúe a continuación. Es posible,
mediante funciones de la biblioteca, que el programador mueva el apuntador para escribir
en la posición que quiera. Al abrir el fichero el apuntador siempre apunta al primer carácter
del fichero, a no ser que lo abramos en modo añadir ("a"); entonces el apuntador apunta a
la posición inmediatamente posterior al último carácter del fichero.
Los ficheros que contienen solamente caracteres de texto se pueden crear fácilmente con
las funciones fgets, fputs, fprintf y fscanf (que son análogas a las correspondientes gets,
puts, printf y scanf). El formato de las dos primeras funciones es el siguiente:
fputs(cadena,puntero-archivo); fgets(cadena,m,puntero-archivo);
fputs devuelve EOF si no puede escribir la cadena de caracteres, y un valor positivo en
caso de tener éxito su tarea. Fgets lee una cadena del fichero hasta que encuentra un
carácter '\n' o bien cuando le m-1 caracteres, siendo m el segundo argumento que se le
pasa. Si hubo un error durante la lectura devuelve NULL; en caso contrario devuelve un
puntero a la cadena de caracteres donde almacenó los valores leídos. A continuación
mostramos el código típico para usar la función fgets:
char cad[100]; FILE *in; in=fopen(....); while(fgets(cad,100,in)!=NULL) /*leemos mientras no devuelva NULL*/ { ...... }
Programación 65/92
Las funciones fprintf y fscanf son totalmente equivalentes a printf y scanf:
FILE *f1,*f2; int a; char b; f1=fopen("ficheronuevo.dat","w"); f2=fopen("ficheroantiguo.dat","r"); .............. fscanf(f2,"%d %c",&a,&b); /* leemos del fichero antiguo y guardamos los valores en las variables a y b */ fprintf(f1,"%d %c\n",a,b); /* escribimos en el fichero nuevo los valores de las variables a y b */
A continuación presentamos un programa que copia un archivo de texto en otro empleando
estas funciones:
/* *ejemplo6_28.c */ #include <stdio.h>
int main(void) { FILE *fent; FILE *fsal; char car[120]; int res = 0; char * ret; fent = fopen("./entrada.txt", "r"); if (fent == NULL) { fprintf(stderr, "Error abriendo entrada.txt \n"); return(0); } fsal = fopen("./salida.txt", "w"); if (fsal == NULL) { fprintf(stderr, "Error creando salida.txt \n"); fclose(fent); return(0); } do { ret = fgets(car, 110, fent); if ( car == NULL)
Programación 66/92
fprintf(stderr, "Error al leer \n"); else fprintf(stdin, "Longitud linea leida: %d \n", strlen(car)); /* Escritura de la línea */ if (ret != NULL) { res = fputs(car, fsal); if (res == EOF) fprintf(stderr, "Error al escribir %s \n", car); } } while (ret != NULL); fclose(fent); fclose(fsal); system("pause"); return(0); }
8.3 Lectura y escritura de bloques
Las funciones fread y fwrite permiten leer/escribir un bloque entero de bytes. fwrite
recibe como parámetros la dirección del bloque donde se encuentran los datos a escribir al
fichero, el número de bytes que ocupa un bloque, el número de bloques a transferir y el
manejador del fichero. Los datos se escriben en el fichero en formato binario (esto es, un
flotante se escribirá mediante 4 bytes que son su codificación en binario y si intentamos
visualizar el fichero con un editor de texto veremos caracteres extraños) FILE *f; float k=3.2; f=fopen("fichero","wb"); fwrite(&k,sizeof(float),1,f);
fread es una función equivalente a fwrite pero lee datos desde el fichero en lugar de
escribir al fichero. Los datos leídos del fichero se guardan en el bloque indicado mediante
el primer parámetro:
FILE *f; float k; f=fopen("fichero","rb"); fread(&k,sizeof(float),1,f);
Programación 67/92
Las funciones son muy útiles para leer y escribir estructuras enteras (de este modo se evita
ir leyendo/escribiendo individualmente cada uno de sus miembros):
struct cuenta { int saldo; char nombre[80]; }; FILE *f; struct cuenta cliente; f=fopen("fichero","rb"); fread(&cliente,sizeof(struct cuenta),1,f); /* leemos el registro completo */
Tanto fread como fwrite devuelven como resultado el número de bloques que han sido
leídos o escritos de forma correcta o un número negativo si ha sucedido algún error.
Debemos tener cuidado con los punteros como miembros de las estructuras: si la estructura
fuese definida como:
struct cuenta { int saldo; char *nombre; };
entonces al escribir una variable de tipo struct cuenta a un fichero escribiríamos el valor
del puntero (esto es, la dirección de memoria) y lo que hay que escribir es la cadena de
caracteres completa. Al tratar de leer esa estructura (probablemente en otra ejecución del
mismo programa) leeríamos en el campo nombre una dirección de memoria.
A continuación mostramos un programa que escribe en registros telefónicos que se piden
al usuario a través de la entrada estándar en un archivo:
/* *ejemplo6_29.c */ #include <stdio.h> struct {
Programación 68/92
char nombre[20]; char apellido[20]; char telefono[15]; } registro; int main() { FILE *fichero; fichero = fopen( "nombres.txt", "ab" ); if(fichero!=NULL){ do { printf( "Introduzca el nombre: " ); gets(registro.nombre); if (strcmp(registro.nombre,"")) { printf( "Introduzca el apellido: " ); gets(registro.apellido); printf( "Teléfono: " ); fflush(stdout); gets(registro.telefono); fwrite( ®istro, sizeof(registro), 1, fichero ); } } while (strcmp(registro.nombre,"")!=0); fclose( fichero ); } else { printf( "Error al abrir el fichero" ); } }
El programa que mostramos a continuación permite la lectura de del archivo generado por
el programa anterior:
/* *ejemplo6_30.c */ #include <stdio.h> struct { char nombre[20]; char apellido[20]; char telefono[15]; } registro; int main() { FILE *fichero;
Programación 69/92
fichero = fopen( "nombres.txt", "rb" ); if(fichero!=NULL){ while (!feof(fichero)) { if (fread( ®istro, sizeof(registro), 1, fichero )!=0) { printf( "Nombre: %s\n", registro.nombre ); printf( "Apellido: %s\n", registro.apellido); printf( "Teléfono: %s\n", registro.telefono); } } fclose( fichero ); } else { printf( "Error abriendo el fichero" ); } }
8.3.1 feof
La función feof nos permite determinar si hemos llegado al final de un fichero (en
operaciones de lectura). feof devuelve un valor distinto de cero cuando se llega al final del
fichero:
FILE *f; f=fopen("fichero","rb"); while(!feof(f)) { fread(....) ..... }
8.3.2 rewind
Esta función sirve para mover el apuntador al comienzo del fichero (por ejemplo, cuando
hemos llegado al final del fichero leyendo datos y queremos volver al principio).
FILE *f; ........ rewind(f);
Programación 70/92
8.3.3 fseek
Permite posicionar el apuntador en la posición que le especifiquemos:
fseek(f,desplazamiento,origen);
desplazamiento es el número de bytes a mover el apuntador; origen es una de las siguientes
constantes del sistema:
• SEEK_SET: el desplazamiento especificado se realiza desde el comienzo del
archivo.
• SEEK_CUR: el desplazamiento especificado se realiza desde la posición actual del
apuntador.
• SEEK_END: el desplazamiento se realiza desde el final del fichero.
8.3.4 ftell
Devuelve un entero largo que indica la posición dentro del fichero del apuntador (número
de bytes desde el inicio del fichero donde se encuentra el apuntador).
long a; FILE *f; a=ftell(f);
8.4 Ejemplo de entrada y salida
A continuación mostramos una implementación de una pequeña agenda electrónica que
permite crear registros con números de teléfonos, modificarlos, buscarlos, eliminarlos y
almacenarlos en el disco duro.
/* *ejemplo6_31.c *listin.h */ #define LNOMBRE 30 /*longitud de la cadena nombre*/ #define LDIRECC 40 /*longitud de la cadena direccion*/ #define LTFNO 12 /*longitud de la cadena telefono*/
Programación 71/92
/*DEFINICION DE LA ESTRUCTURA*/ typedef struct dir{ char nombre[LNOMBRE]; char direccion[LDIRECC]; char telefono[LTFNO]; } ficha; extern void inicializar_listin (ficha *listin, const char *fichero, int tamano); extern void introducir_usuario(ficha *listin); extern int buscar_usuario (ficha *listin); extern void borrar_usuario (ficha *listin); extern void modificar_usuario (ficha *listin); extern void salvar (ficha *listin, const char *fichero); /* *ejemplo7_31 *principal.c */ #include <stdio.h> #include <stdlib.h> #include "listin.h" /*DECLARACIONES DE CONSTANTES*/ #define MAX 120 /*longitud del array de estructuras*/ #define PANTALLA 5 /*numero de usuarios en pantalla*/ #define FIN 7 /*numero de opciones del menú*/ /*DEFINICION DEL ARRAY DE PERSONAS*/ ficha listin[MAX]; /*NOMBRE DEL ARCHIVO SOBRE EL QUE SE TRABAJA*/ char nombreArchivo[100]="listin.dat"; /*DECLARACIONES DE FUNCIONES*/ int imprime_menu(const char *fichero); void imprimir_listin (const char *fichero); void tecla(void); /***************PROGRAMA PRINCIPAL**************/ int main() { int opc=0; inicializar_listin(listin, nombreArchivo, MAX); while (1) { switch (opc=imprime_menu(nombreArchivo)) { case 1: introducir_usuario(listin); break; case 2: borrar_usuario(listin); break; case 3: imprimir_listin(nombreArchivo); break;
Programación 72/92
case 4: buscar_usuario(listin); break; case 5: modificar_usuario(listin); break; case 6: salvar(listin, nombreArchivo); break; case 7: exit(0); } tecla(); } return 0; } /********************DEFINICION DE FUNCIONES********************/ /*imprime el menu y devuelve la opcion del usuario*/ int imprime_menu(const char *fichero) { char s[80]; int c=0; printf("------------------------ NOMBRE DEL LISTIN: %s ---------------------\n", fichero); printf("\n\t\t\tÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ\n"); printf("\t\t\tº1.Introducir usuario º\n"); printf("\t\t\tº2.Borrar usuario º\n"); printf("\t\t\tº3.Imprimir listin º\n"); printf("\t\t\tº4.Buscar usuario º\n"); printf("\t\t\tº5.Modificar usuario º\n"); printf("\t\t\tº6.Guardar º\n"); printf("\t\t\tº7.Salir º\n"); printf("\t\t\tÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍͼ\n"); do { printf("-------Introducir opcion [1-%d]: ",FIN); gets(s); c=atoi(s); }while (c<0 || c>FIN); system("cls"); return c; } /*lista los usuarios no borrados, controlando el llenado de pantalla*/ void imprimir_listin (const char *fichero) { register int i=0,j; printf("------------------------ NOMBRE DEL LISTIN: %s ------------------------\n", fichero); for (j=0; j<MAX; j++) if (listin[j].nombre[0]!=0) { if (i==PANTALLA)
Programación 73/92
{ i=1; tecla(); printf("------------------------ NOMBRE DEL LISTIN: %s ------------------------\n", fichero); } else i++; printf("NOMBRE: %s\n",listin[j].nombre); printf("DIRECCION: %s\n",listin[j].direccion); printf("TELEFONO: %s\n",listin[j].telefono); printf("------------------------\n"); } } /*Espera que el usuario pulse una tecla para continuar y borra la pantalla*/ void tecla(void) { printf ("\nPulse una tecla para continuar..."); getchar() ; system("cls"); } /* *ejemplo6_31.c *listin.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include "listin.h" int MAX=100; /*carga el fichero en el array listin*/ void inicializar_listin (ficha *listin, const char *fichero, int tamano) { MAX = tamano; FILE *fp; register int i=0; if ((fp=fopen(fichero,"rb")) == NULL) printf ("La lista telefonica esta vacia. Comience la insercion de datos\n"); else { while (fread (&listin[i], sizeof(ficha),1,fp) && i<MAX)
Programación 74/92
i++; fclose(fp); printf("Datos leidos con exito\n"); } for (; i<MAX; i++) /*inicializar el resto de componentes del array*/ listin[i].nombre[0]='\0'; } /*Una vez localizado el usuario a modificar, pide nuevos datos*/ void modificar_usuario(ficha *listin) { register int i; int posicion=0; char cad[255]; ficha usu; printf("\n\n\n\t\t\tPara modificar..."); posicion=buscar_usuario(listin); if (posicion!=-1) { printf("Introduce el nuevo nombre [INTRO para conservar el antiguo]: "); gets(cad); if (strlen(cad)!=0) strcpy(usu.nombre,cad); else strcpy(usu.nombre, listin[posicion].nombre); printf("Introduce la nueva dirección [INTRO para conservar la antigua]: "); gets(cad); if (strlen(cad)!=0) strcpy(usu.direccion,cad); else strcpy(usu.direccion, listin[posicion].direccion); printf("Introduce el nuevo telefono [INTRO para conservar el anterior]: "); gets(cad); if (strlen(cad)) strcpy(usu.telefono,cad); else strcpy(usu.telefono, listin[posicion].telefono); listin[posicion]=usu; printf(" en los nueve El usuario ha sido modificado con éxito\n"); } } /*Pide el nombre del usuario y lo busca en el listin*/ int buscar_usuario (ficha *listin)
Programación 75/92
{ char inicial[LNOMBRE]; register int i; printf("\n\n\n\t Introduce el nombre a buscar"); gets(inicial); printf("\n"); for (i=0; i<MAX; i++) if (strcmp(listin[i].nombre,inicial)==0) { printf("NOMBRE: %s\n",listin[i].nombre); printf("DIRECCION: %s\n",listin[i].direccion); printf("TELEFONO: %s\n",listin[i].telefono); printf("------------------------\n"); break; } if (i==MAX) { printf ("El usuario no se encuentra\n"); i=-1; } return (i); } /*Guarda en el fichero asignado todos los usuarios menos los borrados*/ void salvar (ficha *listin, const char *fichero) { FILE *fp; register int i=0; if ((fp=fopen(fichero,"wb")) == NULL) printf ("No se pueden guardar datos en el fichero de datos\n"); else { for (i=0;i<MAX;i++) if (listin[i].nombre[0]) /*componente no vacia*/ if ((fwrite (&listin[i], sizeof(ficha),1,fp)) != 1) { printf("Error al escribir en el listin asignado\n"); return; } fclose(fp); printf("Datos guardados con exito\n"); } } /*busca el primer hueco vacio en el array y pide los datos*/ void introducir_usuario(ficha *listin) {
Programación 76/92
char cad[255]; int i,j,bien; ficha usu; for (j=0; j<MAX; j++) { if (listin[j].nombre[0]=='\0') break; } printf("\n\n\n\n"); if (j==MAX) printf("El listin telefonico esta lleno, no se puede introducir\n"); else { printf("Introduce el nombre: "); gets(cad); strcpy(usu.nombre,cad); printf("Introduce la dirección: "); gets(cad); strcpy(usu.direccion,cad); printf("Introduce el telefono: "); gets(cad); strcpy(usu.telefono,cad); listin[j]=usu; printf ("El usuario ha sido introducido con éxito\n"); } } /*Localiza el usuario a borrar, pide confirmación y lo elimina*/ void borrar_usuario(ficha *listin) { int posicion=0; char c=' '; printf("\n\n\n\t\t\tPara borrar..."); posicion=buscar_usuario(listin); if (posicion!=-1) { printf("\n¨Esta seguro que quiere borrar este usuario? [s/n]"); if ((c=getchar())=='s'||c=='S') { listin[posicion].nombre[0]='\0'; printf ("El usuario ha sido eliminado\n"); } else printf ("El usuario NO ha sido borrado\n"); getchar(); } }
Programación 77/92
9 Enumeraciones
Una enumeración es un tipo de datos cuyos posibles valores son constantes que están
escritas como identificadores:
enum palos {oros, copas, espadas, bastos}; enum palos b1,b2; b1 = copas; b2 = espadas;
Los nombres incluidos entre llaves representan los valores que se pueden asignar a las
variables que sean del tipo enumeración. Estos nombres tienen que ser diferentes entre sí y
diferentes de los identificadores cuyo ámbito sea el mismo que el de la enumeración (por
ejemplo, no puede existir una variable con el nombre oros en el mismo ámbito que la
enumeración).
Es posible declarar a la vez la enumeración y las variables (como en estructuras y
uniones):
enum colores {negro, azul, rojo, verde, amarillo, blanco} primerplano,fondo;
También es posible definir enumeraciones anónimas:
enum {negro, azul, rojo, verde, amarillo, blanco} primerplano, fondo;
Internamente las enumeraciones son manejadas como variables enteras en las que el primer
elemento de la enumeración se corresponde con el valor 0 (negro) y el último con el valor
5 (blanco). Por tanto, es equivalente escribir primerplano=negro o escribir primerplano=0.
No es conveniente utilizar los números para asignarle valores a variables de tipo
enumeración, ya que es muy fácil cometer fallos si lo hacemos, entre otros motivos porque
muchos compiladores permiten también asignar valores enteros fuera del rango
especificado por la enumeración: fondo=27.
También es posible asignar automáticamente una constante entera a uno o varios miembros
de la enumeración. Por ejemplo:
Programación 78/92
enum colores {negro=-2, azul, rojo, verde, amarillo, blanco};
Asigna los siguientes valores a sus constantes:
negro -2 azul -1 rojo 0 verde 1 amarillo 2 blanco 3
mientras que
enum colores {negro=-2, azul, rojo, verde, amarillo, blanco=2};
Asigna los siguientes valores:
negro -2 azul -1 rojo 0 verde 1 amarillo 2 blanco 2
Nótese que ahora hay dos colores asociados al entero 2.
Como internamente las variables de tipo numeración son enteros, se pueden comparar
enumeraciones:
enum {negro, azul, rojo, verde, amarillo, blanco} primerplano, fondo; primerplano=azul; fondo=rojo; ........................ if (primerplano > fondo) { ............. }
Si bien se dispone de la misma funcionalidad con el uso de variables enteras, las variables
enumeración suelen utilizarse en programas grandes para facilitar la legibilidad del código
(muchas veces es mucho más intuitivo comprender una expresión como fondo=azul que
algo como fondo=34). Es bastante útil el uso de typedef junto con enumeraciones:
Programación 79/92
typedef enum {europeos, africanos, asiaticos, americanos} regiones; regiones a1, a2;
A continuación mostramos un pequeño código de ejemplo donde se emplean
enumeraciones:
/* *ejemplo6_32.c */ void main ( ) { enum diasemana {lunes, martes, miercoles, jueves, viernes, sabado, domingo }; int dia; for ( dia = lunes; dia <= domingo; dia++ ) { if ( dia!= sabado && dia != domingo ) printf("El día %d toca trabajar\n",dia); else printf("El día %d toca descansar\n",dia); } }
10 Macros
Hasta ahora la directiva del preprocesador define sólo la hemos utilizado para definir
constantes simbólicas. También puede usarse para definir macros. Informalmente,
podemos considerar a una macro como un identificador simple que es equivalente a una
expresión o instrucción completa, o a varias expresiones e instrucciones completas. En
cierto sentido, son similares a las funciones pero, no obstante, las macros son definidas y
tratadas de forma diferente que las funciones durante el proceso de compilación.
/* *ejemplo6_33.c
Programación 80/92
*/ #include <stdio.h> #define superficie longitud * ancho main() { int longitud, ancho; printf("Dame la longitud:"); scanf("%d",&longitud); printf("Dame el ancho:"); scanf("%d",&ancho); printf("La superficie es: %d",superficie); system("pause"); }
En este ejemplo la macro superficie representa la expresión longitud * ancho. Se
reemplazan todas las ocurrencias de superficie por la expresión longitud * ancho.
El ámbito de actuación de una macro se restringe al archivo en el que aparece definida.
Dentro de un archivo no se pueden reconocer macros definidas en otros archivos. Es
posible escribir macros de varias líneas introduciendo un carácter \ al final de cada línea (a
excepción de la última).
/* *ejemplo6_34.c */ #include <stdio.h> #define bucle for (lineas=1; lineas <=n; lineas++) { \ for (cont=1;cont <= n-lineas;cont++) \ putchar(' '); \ for (cont=1;cont <= 2*lineas-1 ;cont++) \ putchar('*'); \ printf("\n"); \ } main() { int cont, lineas, n; printf("numero de lineas = "); scanf("%d",&n); printf("\n");
Programación 81/92
bucle; system("pause"); }
Es posible definir macros que se configuren mediante una serie de parámetros:
#define md1 (i,m,n) \ factor=1+ i / m; \ razon=12*(pow(factor,m*n) - 1) / i; .... main() { ...... double a,b,c; ..... md1(a,b,c); }
Las principales desventajas del uso de macros con respecto a las funciones es que las
macros comprueban el número de argumentos pero no su tipo y que no se pueden pasar
macros como argumentos de funciones. Las ventajas de las macros es que son más
eficientes que las funciones, ya que el código de la macro antes de compilar se copia
literalmente dentro de la función que la invoca y de este modo se evita realizar una llamada
a una función.
Debemos tener en cuenta que la sustitución que se efectúa al invocar una macro es textual,
por tanto si definimos
#define raiz(a,b) sqrt (a*a + b*b)
invocamos la macro de la forma:
raiz(x+1,y+2)
se evaluaría:
sqrt ( x+1 * x +1 + y+2 * y+2)
Programación 82/92
Obviamente, este resultado no es el deseado. El problema es que la sustitución no
introduce paréntesis, por lo que es necesario que el programador los introduzca
explícitamente:
#define raiz(a,b) sqrt ((a)*(a) + (b)*(b))
11 Más directivas del preprocesador
Como ya hemos comentado, el preprocesamiento es el primer paso en la étapa de
compilación de un programa de C. El preprocesador el cual puede ser una herrramienta útil
para el programador. El preprocesador permite hacer programas más fáciles de leer y de
modificar así como incrementar la portabilidad del código entre diferentes arquitecturas de
máquinas.
11.1 #define
Esta directiva, que ya hemos usado para definir nuevos tipos de variable, constantes y
macros, también permite configurar el lenguaje. Por ejemplo, para cambiar los
delimitadores de bloque de código { ... } por otros delimitadores que haya inventado el
programador como "inicio" y "fin" se puede hacer:
#define inicio { #define fin }
El preprocesador se encargará de que todas las ocurrencias de inicio y fin sean
reemplazadas por su correspondiente { o } delimitador y las siguientes etapas de
compilación de C no encontrarán ninguna diferencia.
11.2 #undef
Permite eliminar una definición previa. Su sintaxis es:
#undef <nombre de macro>
El uso principal de #undef es permitir localizar los nombres de macros sólo en las
secciones de código que los necesiten.
Programación 83/92
11.3 #if Inclusión condicional
La directiva #if evalúa una expresión constante entera y realiza una acción en caso de que
sea cierta. Siempre se debe terminar con #endif para delimitar el fin de esta sentencia. Se
puede evaluar otro código en caso se cumpla otra condición, o bien, cuando no se cumple
ninguna, usando #elif o #else respectivamente. Veamos un ejemplo:
#define MEX 0 #define EUA 1 #define FRAN 2 #define PAIS_ACTIVO MEX #if PAIS_ACTIVO == MEX char moneda[]="pesos"; #elif PAIS_ACTIVO == EUA char moneda[]="dolar"; #else char moneda[]="franco"; #endif
Las directivas #ifdef (si definido) y #ifndef (si no definido) ejecutan una serie de acciones
dependiendo de que esté o no definida una macro. La sintaxis de #ifdef es:
#ifdef <nombre de macro> <secuencia de sentecias> #endif
Si el nombre de macro ha sido definido en una sentencia #define, se compilará la secuencia
de sentecias entre el #ifdef y #endif. La sintaxis de #ifdef es:
#ifndef <nombre de macro> <secuencia de sentecias> #endif
Las directivas anteriores son útiles para revisar si ciertas macros están definidas. Por
ejemplo, para poner el tamaño de un entero para un programa portable entre TurboC de
DOS y un sistema operativo con UNIX, sabiendo que TurboC usa enteros de 16 bits y
Programación 84/92
UNIX enteros de 32 bits, entonces si se quiere compilar para TurboC se puede definir una
macro TURBOC, la cual será usada de la siguiente forma:
#ifdef TURBOC #define INT_SIZE 16 #else #define INT_SIZE 32 #endif
11.4 Control del preprocesador del compilador
Se puede usar el compilador gcc para controlar los valores dados o definidos en la línea de
comandos. Esto permite alguna flexibilidad para configurar valores ademas de tener
algunas otras funciones útiles. Para lo anterior, se usa la opción -Dmacro[=defn], por
ejemplo:
gcc -DLONGLINEA=80 -o prog prog.c
Tiene el mismo efecto que
#define LONGLINEA 80
Si no se indica ningún valor:
gcc –DDEBUG -o prog prog.c
El valor que toma es 1. Esto puede ser útil para incluir código de depuración en nuestro
programa:
#ifdef DEBUG printf("Depurando: Versión del programa 1.0\n"); #else printf("Version del programa 1.0 (Estable)\n"); #endif
Como los comandos del preprocesador pueden estar en cualquier parte de un programa, se
pueden filtrar variables para mostrar su valor, cuando se esta depurando:
x = y * 3;
Programación 85/92
#ifdef DEBUG printf("Depurando: variables x e y iguales a \n",x,y); #endif
11.5 Otras directivas del preprocesador
La directiva #error fuerza al compilador a parar la compilación cuando la encuentra. Se usa
principalmente para depuración. Por ejemplo:
#ifdef OS_MSDOS #include <msdos.h> #elifdef OS_UNIX #include "default.h" #else #error Sistema Operativo incorrecto #endif
12 Parámetros de línea de comandos
Al igual que cuando llamamos a una función podemos pasarle una serie de parámetros, la
función main, que es el punto de inicio de nuestro programa, puede recibir parámetros del
sistema operativo. Esto es, cuando lancemos nuestro ejecutable desde el sistema operativo
podemos añadir una serie de parámetros en línea de comandos (por ejemplo desde una
consola de MSDOS).
Se permite recibir solamente dos argumentos que se llaman tradicionalmente argc y argv.
El primero es una variable entera y el segundo es un array de punteros. Para recibir
parámetros en lugar de definir la función principal como:
int main(){ ...... }
Debemos definirla como
int main(int argc,char *argv[]) { }
Programación 86/92
argc contiene el número de argumentos que el usuario utilizó para invocar al programa. Si
el usuario sólo escribe el nombre del ejecutable para invocar al programa entonces argv
tendrá el valor 1. Por ejemplo, si tenemos un ejecutable sumar.exe, la llamada:
c:\midirectorio> sumar
hará que en el main de sumar recibirá 1 como valor para argc. Si, por contra, el usuario
realiza una invocación del tipo:
c:\midirectorio> sumar 4 5
en el main de sumar se reciba 3 como valor para argc. En el segundo argumento, argv, el
sistema operativo introduce, como cadenas de caracteres, los parámetros que introdujo el
usuario. Por ejemplo:
c:\midirectorio> sumar 4 5
Produce que argc=3, y argv[0] contiene la cadena "sumar", argv[1] contiene la cadena "4"
y argv[2] contiene la cadena "5". Por tanto, la estrategia básica que siguen muchos
programas es leer el valor de argc (para ver que realmente se corresponde con el número
de argumentos de línea que esperan) y, a continuación, extraer las cadenas de argv.
Veamos un ejemplo:
/* *ejemplo6_35.c */ #include <stdio.h> main(int argc,char *argv[]) { int n1,n2; if (argc!=3) { printf("Error en la linea de comandos\n\n"); printf("Formato que debe usar:\t %s <numero> <numero>\n\n",argv[0]); exit(0); } n1=atoi(argv[1]); n2=atoi(argv[2]); printf("Resultado: %d",n1+n2); system("pause"); }
Programación 87/92
Si invocamos al programa como "SUMA" la salida de consola será:
Error en la linea de comandos
Sigue invocamos como " SUMA 34 40" la salida que obtendremos será:
Resultado: 74
Los argumentos de la línea de comandos se suelen utilizar para que el usuario especifique,
por ejemplo, nombres de ficheros que se utilizarán internamente por el programa.
Programación 88/92
13 Ejercicios
13.1 Entrada y salida de pantalla
1. Escribe un programa que lea dos números enteros de teclado y aplique sobre ellos
todos los operadores aritméticos para datos de tipo entero.
2. Escribe una función que calcule los pagos mensuales de una hipoteca a partir del
capital del préstamo, el interés anual y el número de años. Todos estos datos deben
introducirse por teclado. Para este cálculo se emplea la fórmula: cuota=
N
R
RC
)1
1(1+
−
⋅ donde C es el capital del préstamo, R es la tasa de interés mensual en
tanto por uno y N es el número de pagos a realizar.
3. Escribe un programa que imprima el carácter ‘a’ como letra y con un número, y
que a continuación pida al usuario un número entero e imprima su carácter
asociado.
4. Escribe un programa que asigne a una variable entera el valor 30 y a una variable
real el valor 5, 123. Suma ambas variables e imprímelas como un dato real, un dato
entero y un carácter.
5. Escribe un programa que le pida al usuario un número real, calcule su raíz
cuadrada, la redondee al entero más próximo e imprima los resultados de la raíz
cuadrada y el redondeo.
6. Escribe un programa que reciba desde el teclado un número de horas y calcule las
semanas, días y horas que representan.
13.2 Punteros
7. Escribe un programa que lea de teclado un dato de tipo int y lo convierta en dos
datos de tipo short empleando la aritmética de punteros.
8. Escribe un programa que reserve dinámicamente memoria para 512 caracteres y
que a continuación muestre por pantalla el contenido de esas direcciones memoria
reservadas.
Programación 89/92
9. Usando punteros, escribe un programa que solicite al usuario un número de datos
N, reserve memoria dinámicamente para ellos y calcule su media, el mínimo y el
máximo.
10. Escribir un programa que defina una variable entera; un puntero que almacenará la
dirección de memoria de dicha variable entera; un puntero que almacene la
dirección de memoria del anterior puntero; y un puntero que almacene la dirección
de memoria del puntero anterior. Empleando únicamente la tercera variable puntero
mostrar: el contenido y la dirección de memoria del segundo puntero; el contenido
la dirección de memoria del primer puntero y el contenido la dirección memoria
correspondiente con la variable entera.
11. Escribir un programa que reserve dinámicamente memoria para 15 datos de tipo
entero; el programa debe inicializar aleatoriamente las 15 posiciones de memoria
reservadas y mostrarlas por pantalla. A continuación se solicita al usuario un
número y se eliminan todas las ocurrencias de dicho número en la memoria
reservada; para ello se desplaza hacia atrás el resto de los números. A continuación,
se vuelven a mostrar al usuario los números restantes y se le vuelve a solicitar otro
número a borrar. El programa finaliza o bien cuando el usuario introduzca como
entrada a la letra "x" o bien cuando sean eliminados todos los datos.
13.3 Arrays
12. Escribir un programa que pida el grado de un polinomio, sus coeficientes y un
valor para "x" y que calcule el valor del polinomio en x. Emplear un array para
almacenar los coeficientes del polinomio.
13. Empleando un array, escribir un programa que pida al usuario números enteros
hasta que se introduzca el número 0. A continuación, calcular la media, el mínimo
y el máximo de los datos introducidos.
14. Escribir un programa que solicite al usuario dos vectores de N elementos y que
imprima su producto escalar.
15. Escribir un programa que rellene una matriz cuadrada de hasta 10 x 10 (las
dimensiones de la matriz serán un parámetro que se pida al usuario) con números
aleatorios de tal modo que la matriz sea simétrica. Imprimir la matriz por pantalla.
Programación 90/92
16. Escribir un programa que calcule la traspuesta de la matriz. La dimensión y los
valores de la matriz se deben pedir al usuario por teclado.
17. Hacer un programa que calcule la suma de los elementos del triángulo superior y
del inferior de una matriz (la diagonal principal está incluida en ambos casos).
18. Escribir un programa que calcule la solución a un sistema de N ecuaciones lineales
con N incógnitas por el método de Gauss- Jordan, consistente en hacer ceros el
triángulo inferior de la matriz.
19. Escribir un programa que multiplique dos matrices. Sus dimensiones y valores
deben de solicitarse al usuario por teclado y tras realizar la multiplicación debe
visualizarse en pantalla ambas matrices y el resultado de la multiplicación.
20. Reescribir los ejercicios 22, 24 y 27 emplean de reserva dinámica de memoria y
accediendo las posiciones de memoria reservadas mediante la aritmética de
punteros.
13.4 Cadenas de caracteres
21. Escribe un programa que acepte una cadena de caracteres (que podrá contener
cualquier carácter a excepción del retorno de carro) y que diga cuántas vocales
contiene.
22. Escribe un programa que acepte una cadena de caracteres (que podrá contener
cualquier carácter a excepción del retorno de carro) y que le escriba al revés.
23. Escribe un programa que pida dos cadenas de caracteres al usuario y que informe si
ambas cadenas son iguales (se considera que las cadenas son iguales aunque
difieren en el uso de letras mayúsculas y minúsculas), que imprima por pantalla el
resultado de concatenar ambas cadenas de caracteres y los 15 primeros caracteres
de la cadena concatenada.
24. Escribe un programa que devuelva el número de caracteres que hay entre la
primera y la última aparición de un carácter dado en una cadena.
25. Escribe un programa que lee una cadena de caracteres de teclado e indique si esa
no palíndroma (se lee igual de izquierda a derecha que de derecha a izquierda, sin
tener en cuenta los espacios en blanco y las mayúsculas). Por ejemplo: "dábale
arroz a la zorra el abad".
Programación 91/92
26. Escribe un programa que pida dos cadenas de caracteres al usuario y cuente cuántas
veces está contenida la segunda cadena de caracteres en la primera, empleando para
ello punteros.
13.5 Estructuras y uniones
27. Crear una estructura que representa un empleado. En ella se deberá contener su
nombre, dirección, número teléfono, edad, sexo, salario y fecha de ingreso en la
empresa. Para representar la fecha empléese otra estructura.
28. Escribir un programa que permita crear fichas de empleado según la estructura
diseñada en el ejercicio anterior. El programa debe de solicitar continuamente datos
al usuario que le permitan crear empleados, debe realizar una validación de estos
datos (por ejemplo, que el número de teléfono no contiene caracteres) y debe
permitir la terminación del programa mediante algún código.
29. Modificar un programa anterior para que, además de introducir empleados, permita
realizar un listado de todos los empleados disponibles y para que compruebe antes
de añadir un empleado si existe otro que tenga el mismo nombre.
13.6 Entrada y salida
30. Escribe un programa que pida sucesivamente líneas de caracteres al usuario hasta
que este introduzca una "x". Las líneas de caracteres las irá almacenando en un
archivo de texto.
31. Modificar el programa anterior añadiendo una opción que permita realizar un
listado del archivo de texto. El programa constará de un primer menú donde el
usuario indica si desea listar el contenido del archivo que está en el disco duro o
introducir más líneas de texto. Si realiza el listado, tras mostrar las líneas por
pantalla se vuelve al menú principal. Si está introduciendo líneas de texto y pulsa
"x" se vuelve al menú principal. Asegúrese que el archivo no se machaca si ya
existe y que las nuevas líneas se añaden al final del archivo.
32. Escribe un programa que pida el nombre de dos archivos al usuario y copie el
contenido del primero en el segundo.
Programación 92/92
33. Escribe un programa que pida el nombre de dos archivos al usuario y compruebe si
ambos archivos son o no iguales.
34. Escribe un programa que lea un número de palabras que contiene un archivo de
texto cuyo nombre será introducido por el usuario.
35. Escribe un programa que pida un conjunto de datos reales al usuario y los almacene
en un archivo en formato binario. Cuando generes el archivo visualiza su contenido
con un editor de textos.
36. Modifica al programa anterior de tal modo que el usuario pueda elegir entre
introducir más números, que se añadirán al final del archivo en caso de que éste
exista, o listar los números que contiene el archivo.
37. Escribe un programa que reciba como argumentos un archivo de texto y una
palabra a buscar en ese archivo. El programa debe contar el número de veces que
aparece la palabra en el archivo.
38. Modifique el programa del ejercicio 37 para permitir que los registros empleado se
almacenen en el disco duro un fichero. Se deberá de incluir otra opción en el menú
que permita guardar los cambios al archivo. Cuando el usuario indique que desea
salir del programa si hay cambios sin guardar se le debe de advertir de esta
circunstancia y darle la oportunidad de guardarlos o proceder sin guardarlos.
39. Modificar el programa anterior para que permita borrar registros.