sábado, 22 de febrero de 2014

Arrays en el Lenguaje C


Una posible definición de array sería:

Un conjunto de datos del mismo tipo, identificados por el mismo nombre, y que se pueden distinguir mediante un número de índice.

Pero ¿qué quiere decir esto y para qué lo queremos?. Pues bien, supongamos que queremos almacenar la temperatura media de cada hora del día y la temperatura promedio del día. Con lo que sabemos hasta ahora podríamos hacer algo así:

#include <stdio.h>

main()
{
 /* Declaramos 24 variables, una para cada hora del dia */
 int temp1,  temp2,  temp3,  temp4,  temp5,  temp6,  temp7,  temp8;
 int temp9,  temp10, temp11, temp12, temp13, temp14, temp15, temp16;
 int temp17, temp18, temp19, temp20, temp21, temp22, temp23, temp0;
 float media;

 /* Asignamos el valor de cada una */
 printf( "Introduzca las temperaturas desde las 0 hasta las 23 
  separadas por un espacio: " );
 scanf( "%i %i %i ... %i", &temp0, &temp1, &temp2, ... &temp23 );

 media = ( temp0 + temp1 + temp2 + temp3 + temp4 + ... + temp23 ) / 24;
 printf( "\nLa temperatura media es %f\n", media );
}


Los puntos ... se utilizan por brevedad en el ejemplo; no constituyen una expresión válida en C.

Observamos que hay que realizar un notable trabajo repetitivo de escritura de código. Precisamente es aquí donde son de utilidad los arrays. Vamos a repetir el programa anterior con un array:

#include <stdio.h>

main()
{
 int temp[24]; /* Con esto declaramos las 24 variables */
 float media;
 int hora;

 /* Ahora damos valor a cada una */
 for( hora=0; hora<24; hora++ ) {
  printf( "Temperatura de las %i: ", hora );
  scanf( "%i", &temp[hora] );
  media += temp[hora];
 }
 media = media / 24;

 printf( "\nLa temperatura media es %f\n", media );
}

El programa resulta más rápido de escribir y más cómodo para el usuario que el anterior.

Como ya hemos comentado, cuando declaramos una variable lo que estamos haciendo es reservar una zona de la memoria para ella. Cuando declaramos el array de este ejemplo es reservar espacio en memoria para 24 variables de tipo int. El tamaño del array (24) lo indicamos entre corchetes al definirlo. Esta es la parte de la definición que dice: Un array es un conjunto de datos del mismo tipo identificados por el mismo nombre.

La parte final de la definición dice: y se distinguen mediante el índice. En el ejemplo recorremos la matriz mediante un bucle for y vamos dando valores a los distintos elementos de la matriz. Para indicar a qué elemento nos referimos usamos un número entre corchetes (en este caso la variable hora), este número es lo que se llama índice del array.

En C, el primer elemento de una matriz tiene el índice 0, el segundo tiene el 1, y así sucesivamente. De modo que si queremos dar un valor al elemento 4 (índice 3) haremos: temp[ 3 ] = 20;

No hay que confundirse. En la declaración del array el número entre corchetes es el número total de elementos; en cambio, cuando usamos la matriz, el número entre corchetes es el índice.

Declaración de un array

La forma general de declarar un array es la siguiente:

tipo_de_dato identificador_del_array[ dimensión ];


donde:
  • El tipo_de_dato es uno de los tipos de datos conocidos (int, char, float, etc.). En el ejemplo era int.
  • El identificador_del_array es el nombre que le damos (en el ejemplo era temp).
  • La dimensión es el número de elementos que tiene el array.

Como se ha indicado antes, al declarar un array reservamos en memoria tantas variables del tipo_de_dato como las indicada en dimensión.

Hemos visto en el ejemplo que tenemos que indicar en varios sitios el tamaño del array: en la declaración, en el bucle for y al calcular la media. Este es un programa pequeño; en un programa mayor probablemente habrá que escribirlo muchas más veces. Si en un momento dado queremos cambiar la dimensión del array tendremos que cambiar todos. Si nos equivocamos al escribir el tamaño (ponemos 25 en vez de 24) cometeremos un error y puede que no nos demos cuenta. Por eso es mejor usar una constante simbólica, por ejemplo NUM_HORAS:


#include <stdio.h>

#define NUM_HORAS 24

main()
{
 int temp[NUM_HORAS]; /* Con esto declaramos las 24 variables */
 float media;
 int hora;

 /* Ahora damos valor a cada una */
 for( hora=0; hora<NUM_HORAS; hora++ ) {
  printf( "Temperatura de las %i: ", hora );
  scanf( "%i", &temp[hora] );
  media += temp[hora];
 }
 media = media / NUM_HORAS;

 printf( "\nLa temperatura media es %f\n", media );
}

Ahora con sólo cambiar el valor de NUM_HORAS una vez lo estaremos haciendo en todo el programa.
Inicialización de un array

En C se pueden inicializar los arrays al declararlos igual que se hace con las variables. Es decir, por ejemplo:

 int hojas = 34;

Así, con arrays se puede hacer:

 int temp[24] = { 15, 18, 20, 23, 22, 24, 22, 25, 26, 25,         24, 22, 21, 20, 18, 17, 16, 17, 15, 14, 14, 14, 13, 12 };


Ahora el elemento 0, el primero, es decir, temperaturas[0], valdrá 15. El elemento 1, el segundo, valdrá 18, y así con todos. Vamos a ver un ejemplo:

#include <stdio.h>

main()
{
 int hora;
 int temp[24] = { 15, 18, 20, 23, 22, 24, 22, 25, 26, 25, 24, 22, 
   21, 20, 18, 17, 16, 17, 15, 14, 14, 14, 13, 12 };

 for( hora=0; hora<24; hora++ ) {
  printf( "La temperatura a las %i era de %i grados.\n", 
   hora, temp[hora] );
 }
}

Pero al introducir los datos no es difícil olvidarse alguno. Hemos indicado al compilador que nos reserve memoria para un array de 24 elementos de tipo int. ¿Qué ocurre si introducimos menos de los reservados? No sucede nada especial, sólo que los elementos que falten se inicializarán a 0.

#include <stdio.h>

main()
{
 int hora;
 int temp[24] = { 15, 18, 20, 23, 22, 24, 22, 25, 26, 25, 24, 22, 
   21, 20, 18, 17, 16, 17, 15, 14, 14 };
   /* Faltan los tres últimos elementos */

 for( hora=0; hora<24; hora++ ) {
  printf( "La temperatura a las %i era de %i grados.\n", 
   hora, temp[hora] );
 }
}

El resultado será:
La temperatura a las 0 era de 15 grados.
La temperatura a las 1 era de 18 grados.
La temperatura a las 2 era de 20 grados.
La temperatura a las 3 era de 23 grados.
...
La temperatura a las 17 era de 17 grados.
La temperatura a las 18 era de 15 grados.
La temperatura a las 19 era de 14 grados.
La temperatura a las 20 era de 14 grados.
La temperatura a las 21 era de 0 grados.
La temperatura a las 22 era de 0 grados.
La temperatura a las 23 era de 0 grados.

Vemos que los últimos 3 elementos son nulos, precisamente aquellos a los que no hemos dado valores iniciales. El compilador no nos avisa de que hemos introducido menos datos de los reservados.

NOTA: Para recorrer del elemento 0 al 23 (24 elementos) hacemos: for(hora=0; hora<24; hora++). La condición es que hora sea menos de 24. También podíamos haber hecho que hora!=24.

Ahora vamos a ver el caso contrario, introducimos más datos de los reservados. Supongamos 25 en vez de 24. Si hacemos esto, dependiendo del compilador obtendremos un error o, al menos, un warning (aviso). En unos compiladores el programa se creará y en otros no, pero aún así nos avisa del fallo. Debe indicarse que estamos intentando guardar un dato de más, no hemos reservado memoria para él.

Si la matriz debe tener una longitud determinada, usamos el método descrito (definiendo el tamaño de la matriz) para definir el número de elementos. En nuestro caso era conveniente, porque los días siempre tienen 24 horas. En otros casos podemos usar un método alternativo: dejar al compilador que cuente los elementos que hemos introducido y reserve espacio para ellos:

#include <stdio.h>

main()
{
 int hora;
 int temp[] = { 15, 18, 20, 23, 22, 24, 22, 25, 26, 25, 24, 
   22, 21, 20, 18, 17, 16, 17, 15, 14, 14 };
   /* Faltan los tres últimos elementos */

 for( hora=0; hora<24; hora++ ) {
  printf( "La temperatura a las %i era de %i grados.\n", 
   hora, temp[hora] );
 }
}

Vemos que no hemos especificado la dimensión del array temp. Hemos dejado los corchetes en blanco. El compilador contará los elementos que hemos puesto entre llaves y reservará espacio para ellos. De esta forma siempre habrá el espacio necesario, ni más ni menos. La pega es que si ponemos más de los que queríamos no nos daremos cuenta.

Para saber en este caso cuantos elementos tiene la matriz podemos usar el operador sizeof. Dividimos el tamaño de la matriz entre el tamaño de sus elementos y tenemos el número de elementos.

#include <stdio.h>

main()
{
 int hora;
 int num_elementos;
 int temp[] = { 15, 18, 20, 23, 22, 24, 22, 25, 26, 25, 24, 
   22, 21, 20, 18, 17, 16, 17, 15, 14, 14 };
   /* Faltan los tres últimos elementos */

 num_elementos = sizeof(temp) / sizeof(int);
 for( hora=0; hora<24; hora++ ) {
  printf( "La temperatura a las %i era de %i grados.\n", 
   hora, temp[hora] );
 }
 printf( "Han sido %i elementos.\n" , num_elementos );
}

Recorrido de un array

En las secciones anteriores veíamos un ejemplo que mostraba todos los datos de un array. Veíamos también lo que pasaba si introducíamos más o menos elementos al inicializar la matriz. Ahora vamos a ver qué pasa si intentamos imprimir más elementos de los que hay en la matriz; en este caso vamos a intentar imprimir 28 elementos cuando sólo hay 24:

#include <stdio.h>

main()
{
 int hora;
 int temp[24] = { 15, 18, 20, 23, 22, 24, 22, 25, 26, 25, 24, 22, 
   21, 20, 18, 17, 16, 17, 15, 14, 14, 14, 13, 12 };

 for( hora=0; hora<28; hora++ ) {
  printf( "La temperatura a las %i era de %i grados.\n", 
   hora, temp[hora] );
 }
}

Lo que se obtiene en mi ordenador es:

La temperatura a las 0 era de 15 grados.
...
La temperatura a las 23 era de 12 grados.
La temperatura a las 24 era de 24 grados.
La temperatura a las 25 era de 3424248 grados.
La temperatura a las 26 era de 7042 grados.
La temperatura a las 27 era de 1 grados.

Vemos que a partir del elemento 24 (incluído) tenemos resultados extraños. Esto es porque nos hemos salido de los límites del array e intenta acceder al elemento temp[24] y sucesivos que no existen. Así que nos muestra el contenido de la memoria que está justo detrás de temp[23](esto es lo más probable), que puede ser cualquiera. Al contrario que otros lenguajes, C no comprueba los límites de los array. Este programa no da error al compilar ni al ejecutar, tan sólo devuelve resultados extraños. Tampoco bloqueará el sistema porque no estamos escribiendo en la memoria sino leyendo de ella.

Otra cosa muy diferente es introducir datos en elementos que no existen. Veamos un ejemplo (se recomienda no ejecutarlo):

#include <stdio.h>

main()
{
 int temp[24];
 float media;
 int hora;

 /* Ahora damos valor a cada una */
 for( hora=0; hora<28; hora++ ) {
  printf( "Temperatura de las %i: ", hora );
  scanf( "%i", &temp[hora] );
  media += temp[hora];
 }
 media = media / 24;

 printf( "\nLa temperatura media es %f\n", media );
}

Lo más probable es que al ejecutar este código el ordenador se bloqueado y deba reiniciarlo. El problema ahora es que estamos intentando escribir en el elemento temp[24], que no existe y puede ser un lugar cualquiera de la memoria. Como consecuencia de esto podemos estar cambiando algún programa o dato de la memoria que no debemos y el sistema se detiene.

Punteros a arrays

Aquí tenemos otro de los importantes usos de los punteros: los punteros a arrays. Ambos están íntimamente relacionados. Para que un puntero apunte a un array se puede hacer de dos formas; una es apuntando al primer elemento del array:

 int *puntero;
 int temp[24];

 puntero = &temp[0];

El puntero apunta a la dirección del primer elemento. Otra forma equivalente, pero mucho más usada es:

 int *puntero;
 int temp[24];

 puntero = temp;

Con esto también apuntamos al primer elemento del array. Obsérvese que el puntero tiene que ser del mismo tipo que el array (en este casoint).

Ahora vamos a ver cómo acceder al resto de los elementos. Para ello empezamos por ver cómo funciona un array: Éste se guarda en posiciones consecutivas en la memoria, de tal forma que el segundo elemento va inmediatamente después del primero. En un ordenador en el que el tamaño del tipo int es de 16 bits (2 bytes) cada elemento del array ocupará 2 bytes. Veamos un ejemplo:

#include <stdio.h>

main()
{
 int i;
 int temp[24];

 for( i=0; i<24; i++ ) {
  printf( "La dirección del elemento %i es %p.\n", i, &temp[i] );
 }
}

Recuérdese que %p sirve para imprimir la dirección de una variable (un puntero) en formato hexadecimal.

El resultado es (en mi ordenador):

La dirección del elemento 0 es FFC6.
La dirección del elemento 1 es FFC8.
La dirección del elemento 2 es FFCA.
La dirección del elemento 3 es FFCC.
...
La dirección del elemento 21 es FFF0.
La dirección del elemento 22 es FFF2.
La dirección del elemento 23 es FFF4.

Vemos aquí que, efectivamente, ocupan posiciones consecutivas y que cada una ocupa 2 bytes. Si lo representamos en una tabla:

transparent4C4384C43A4C43C
temp[0]temp[1]temp[2]temp[3]

Ya hemos visto cómo funcionas los arrays por dentro, ahora vamos a verlo con punteros. Por ejemplo:

#include <stdio.h>

main()
{
 int i;
 int temp[24];
 int *punt;

 punt = temp;

 for( i=0; i<24; i++ ) {
  printf( "La dirección de temp[%i] es %p y la de punt es %p.\n", 
   i, &temp[i], punt );
  punt++;
 }
}

Cuyo resultado es:

La dirección de temp[0] es FFC6 y la de punt es FFC6.
La dirección de temp[1] es FFC8 y la de punt es FFC8.
La dirección de temp[2] es FFCA y la de punt es FFCA.
La dirección de temp[3] es FFCC y la de punt es FFCC.
...
La dirección de temp[21] es FFF0 y la de punt es FFC0.
La dirección de temp[22] es FFF2 y la de punt es FFC2.
La dirección de temp[23] es FFF4 y la de punt es FFC4.

En el código de este ejemplo hay dos líneas importantes (en negrita). La primera es punt = temp;. Con ella hacemos que punt apunte al primer elemento de la matriz. Si no hacemos esto, punt apuntará a un sitio cualquiera de la memoria y debemos recordar que no es conveniente, puede ser desastroso.

La segunda línea importante es punt++. Con esto incrementamos el valor de punt, pero curiosamente, aunque incrementamos una unidad (punt++ equivale a punt=punt+1) el valor aumenta en 2. Aquí se muestra una de las características especiales de los punteros. Recordemos que en un puntero se guarda una dirección. También sabemos que un puntero apunta a un tipo de datos determinado (en este caso int). Cuando sumamos 1 a un puntero sumamos el tamaño del tipo al que apunta. En el ejemplo el puntero apunta a una variable de tipo int que es de 2 bytes; entonces al sumar 1 lo que hacemos es sumar 2 bytes. Con ello se consigue es apuntar a la siguiente posición int de la memoria, en este caso es el siguiente elemento de la matriz.

OperaciónEquivalenteValor de punt
punt = temp;punt = &temp[0];FFC6
punt++;sumar 2 al contenido de punt (FFC6 + 2)FFC8
punt++;sumar 2 al contenido de punt (FFC8 + 2)FFCA

Cuando hemos acabado, estamos en temp[24], que no existe. Si queremos recuperar el elemento 1 podemos hacer punt = temp otra vez o restar 24 a punt: punt -= 24;, con lo que restamos 24 posiciones a punt (24 posiciones*sizeof(int) = 24*2 bytes = 48 bytes).

Vamos a ver ahora un ejemplo de cómo recorrer la matriz entera con punteros y cómo imprimirla:

#include <stdio.h>

main()
{
 int temp[24] = { 15, 18, 20, 23, 22, 24, 22, 25, 26, 25, 24,
   22, 21, 20, 18, 17, 16, 17, 15, 14, 14, 13, 12, 12 };

 int *punt;
 int i;

 punt = temp;
 for( i=0 ; i<24; i++ )
  printf( "Elemento %i: %i\n", i, *punt++ );
}

Cuando acabamos, punt apunta a temp[24], y no al primer elemento, si queremos volver a recorrer la matriz debemos volver como antes al comienzo. Para evitar perder la referencia al primer elemento de la matriz (temp[0]) se puede usar otra forma de recorrer la matriz con punteros:

#include <stdio.h>

main()
{
 int temp[24] = { 15, 18, 20, 23, 22, 24, 22, 25, 26, 25, 24,
   22, 21, 20, 18, 17, 16, 17, 15, 14, 14, 13, 12, 12 };

 int *punt = temp;
 int i;

 for( i=0 ; i<24; i++ )
  printf( "Elemento %i: %i\n", i, *(punt+i) );
}

Con *(punt+i) lo que hacemos es tomar la dirección a la que apunta punt (la dirección del primer elemento de la matriz) y le sumamos iposiciones. De esta forma tenemos la dirección del elemento i. No estamos sumando un valor a punt, para sumarle un valor habría que hacerpunt++ o punt += algo, así que punt siempre apunta al principio de la matriz. Obsérvese que también se ha escrito int *punt=temp; se ha hecho por economía de código y no porque tenga relación con lo que se está explicando.

Se podría hacer este programa sin usar punt. Sustituyéndolo por temp y dejar *(temp+i). Lo que no se puede hacer es: temp++.

Importante: Es preciso comentar que el uso de índices es una forma de maquillar el uso de punteros. El compilador convierte los índices a punteros. Cuando decimos temp[5] en realidad le estamos diciendo *(temp+5) Así que usar índices es equivalente a usar punteros de una forma más cómoda.

Paso de un array a una función

En C no podemos pasar un array entero a una función. Lo que tenemos que hacer es pasar un puntero al array (es decir, la dirección de comienzo del mismo). Con este puntero podemos recorrer el array:

#include <stdio.h>

int sumar( int *m )
{
 int suma, i;

 suma = 0;
 for( i=0; i<10; i++ )
  suma += m[i];

 return suma;
}

main()
{
 int contador;
 int matriz[10] = { 10, 11, 13, 10, 14, 9, 10, 18, 10, 10 };

 for( contador=0; contador<10; contador++ )
  printf( "   %3i\n", matriz[contador] );
 printf( "+ -----\n" );
 printf( "   %3i", sumar( matriz ) );
}

Este programa muestra toda la matriz en una columna. Además, se usa para imprimir los números con formato %3i. El 3 indica que se tienen que alinear los números a la derecha en un espacio de 3 caracteres, así queda más elegante.

Como se ha indicado, no se pasa el array, sino un puntero a ese array. Si probamos a usar sizeof para calcular el número de elementos no funcionará aquí. 

Dentro de la función sumar añadimos la línea:

 printf( "Tamaño del array: %i Kb, %i bits\n", sizeof m, (        sizeof m)*8 );

Devolverá 2 (depende del compilador) que serán los bytes que ocupa el int (m es un puntero a int). ¿Cómo sabemos entonces cual es el tamaño del array dentro de la función? En este caso lo hemos puesto nosotros mismos, 10. Pero, posiblemente, lo mejor es utilizar constantes como se comentó con anterioridad.

Se ha dicho que el array no se puede pasar a una función, sino que se debe usar un puntero. Pero vemos que estamos usando m[i]; esto lo podemos hacer porque, como se ha mencionado antes, el uso de índices es una forma que nos ofrece C de manejar punteros con matrices. Ya se ha visto que m[i] es equivalente a *(m+i).

Otras declaraciones equivalentes serían:

int sumar( int m[] )
ó
int sumar( int m[10] )

En realidad, esta última no se suele usar ya que no es necesario indicar el número de elementos a la función.

int m[] e int *m  son equivalentes.

0 comentarios:

Publicar un comentario

Aprende a Programar tus propias aplicaciones