Tema 6: Introducción a la programación IIbiolab.uspceu.com/aotero/recursos/docencia/TEMA 6.pdf ·...

92
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.

Transcript of Tema 6: Introducción a la programación IIbiolab.uspceu.com/aotero/recursos/docencia/TEMA 6.pdf ·...

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( &registro, 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( &registro, 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.