Guión 4: Arrays


Definición: Estructura de datos formada por una serie de elementos, los cuales son todos del mismo tipo.

El compilador necesita conocer el tipo del array, su alcance (local o global) y el número de elementos que lo componen, con objeto de reservar en memoria el espacio necesario.

Para acceder a cada elemento de un array es necesario conocer su nombre o identificador y la posición que ocupa dicho elemento dentro del mismo, la cual debe indicarse mediante el uso de índices.

Según su dimensión los arrays pueden clasificarse en unidimensionales (vectores) y multidimensionales: bidimensionales (tablas), tridimensionales (cubos), etc.

Los arrays deben tener la longitud mínima necesaria y nunca se deben declarar arrays demasiado grandes.

En C el primer elemento de un array unidimensional tiene como índice el cero. Esta misma regla también se aplica a los arrays multidimensionales.

Ejemplos de declaraciones de arrays:
int num[365]; /* primer elemento: num[0],
                 último elemento: num[364] */

double matriz[10][5]; /* matriz de 50 reales:
                         matriz[0][0]: primera fila y primera columna,
                         matriz[9][4]: última fila y última columna */

char trinitotolueno[70][7][5]; /* array de caracteres tridimensional */
Es posible tener un array local, sólo visible en la función donde se ha declarado, y global, visible por todas las funciones del programa a partir del lugar donde se encuentra la declaración del mismo:
int temp[65]; /* array global de 65 enteros */

int main() {

  double lluvia[200]; /* array local de 200 reales */

  .
  .
  .
}
Al igual que ocurre con las demás variables, los arrays se pueden inicializar al declararlos.

Ejemplo 4.1. Escribir un programa que imprima el número de días que tienen los meses del año.
#include <stdio.h>

int dias[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
/* el compilador cuenta el número de elementos del array y por tanto no es necesario que éste aparezca explícitamente entre los corchetes */


int main() {

  int i = 0;

  for (; i < sizeof(dias)/sizeof(int); i++)
      printf("El mes %d tiene %d dias.\n", i + 1, dias[i]);

  /* en la expresión de test del bucle se calcula el número de elementos del array */


  system("pause");
  return 0;
}
Ejercicio: En el ejemplo 4.1 el tamaño del array se calcula tanto en tiempo de compilación como en tiempo de ejecución, de modo que en caso de que sea necesario modificar el contenido del array, basta con actualizar la declaración de sus elementos, encerrados entre llaves. A partir de dicho ejemplo, eliminar los dos últimos elementos del array, ejecutar el programa una vez compilado y comprobar el resultado. Añadir a la versión original del ejemplo dos nuevos elementos y comprobar de nuevo el resultado de la ejecución.

Cuando los arrays son globales y no han sido inicializados, o lo han sido sólo en parte, el compilador asigna el valor cero a los elementos no inicializados.

Si en la declaración de un array el número de elementos entre llaves excede el tamaño del mismo, el compilador advierte de un error en la inicialización:
char nums[4] = {1, 2, 3, 4, 5}; /* declaración incorrecta */
Se puede asignar un valor a un elemento concreto del array, como si de una variable elemental se tratara.

Ejemplo 4.2. Realizar un programa que almacene en un array local los cincuenta primeros números pares comenzando por el cero y a continuación los muestre por pantalla.

#include <stdio.h>

int main() {

  int contador, pares[50];
 

  for (contador = 0; contador < 50; contador++)
      pares[contador] = 2 * contador;

  for (contador = 0; contador < 50; contador++)
      printf("Par %d: %d.\n", contador, pares[contador]);

  system("pause");
  return 0;
}

Ejercicio: Modificar el ejemplo 4.2 para que imprima los cincuenta primeros números pares en orden inverso.

Ejercicio: Modificar el ejemplo 4.2 para que imprima los cincuenta primeros números impares comenzando por la unidad.

Ejemplo 4.3. Desarrollar un programa que lea del teclado los datos de un array local de cinco enteros y posteriormente los presente al usuario a través de la pantalla.

#include <stdio.h>

int main() {

  int i, nums[5];
 
  for (i = 0; i < 5; i++) {
      printf("Num: ");
      scanf("%d", &nums[i]);
      }

  putchar('\n')

  for (i = 0; i < 5; i++)
      printf("Num: %d.\n", nums[i]);

  system("pause");

  return 0;
}


Punteros a arrays.

Los punteros proporcionan un método simbólico para utilizar direcciones de memoria.

Favorecen la construcción de programas potentes y eficientes por la apoximación a la forma de trabajo del ordenador, cuyas instrucciones máquina emplean abundantemente direcciones de memoria.

En el lenguaje C el nombre o identificador de un array representa la dirección del primer elemento de dicho array:
int antifaz[4] = {5, 6, 7, 8};

/* antifaz == &antifaz[0] */
Ejemplo 4.4. Escribir un programa que permita comprobar que el nombre de un array representa la dirección de su primer elemento.
Nota: Para imprimir el valor de un puntero se debe usar el especificador de formato %p. El valor se muestra en notación hexadecimal (base 16).

#include <stdio.h>

int main() {

  int antifaz[4] = {5, 6, 7, 8};

  printf("antifaz: %p. &antifaz[0]: %p.\n", antifaz, &antifaz[0]);

  system("pause");

  return 0;

}


Paso de arrays como parámetros.

En el lenguaje C, los arrays pueden declararse dentro del cuerpo de una función como variables locales. No obstante, también pueden aparecer como argumentos o parámetros.

Cuando se pasa un array como parámetro, la función llamadora transfiere a la función llamada la dirección de éste. A partir de dicha dirección, la función invocada puede acceder y modificar cualquier elemento del array. Los arrays siempre se pasan a través de sus direcciones.

Ejemplo 4.5. Realizar un programa que haga uso de una función auxiliar para modificar un vector de cinco enteros, el cual ha sido inicializado a ceros en la función principal y es local a ésta. El vector modificado deberá almacenar consecutivamente los enteros del uno al cinco, ambos inclusive. La función principal deberá presentar en pantalla el contenido del vector antes y después de su modificación por la función auxiliar.

#include <stdio.h>

void convierte(int [], int); /* prototipo */

int main() {

  int edad[] = {0, 0, 0, 0, 0}; /* variables locales */
  int i, tam;

  tam = sizeof(edad) / sizeof(int); /* el tamaño se calcula en tiempo de ejecución */

  puts("Antes de invocar a \"convierte()\".\n");
 
for (i = 0; i < tam; i++)
      printf("edad[%d]: %d.\n", i, edad[i]);
  putchar('\n');


  convierte(edad, tam); /* parámetros reales
*/

  puts("Tras invocar a \"convierte()\".\n");
 
for (i = 0; i < tam; i++)
      printf("edad[%d]: %d.\n", i, edad[i]);
  putchar('\n');

  system("pause");
  return 0;
}

void convierte(int primaveras[], int dim) { /* parámetros formales */

  int i; /* variable local */

 
for (i = 0; i < dim; i++) /* se modifica el array */
      primaveras[i] = i + 1;

}

Cuando se emplea una dirección como parámetro, la función trabaja con la dirección de la variable apuntada, y no directamente con el valor. Por tanto, en el ejemplo 4.5, todas las operaciones que afectan al parámetro primaveras[], realmente están trabajando sobre el array edad[] de la función principal.

El array edad[] tiene cinco elementos. Sin embargo, la declaración del parámetro primaveras[] no crea un array, sino una referencia al array edad[], que es el único que existe en el ejemplo y está declarado dentro de la función principal como una variable local a la misma. Cuando en dicha función se realiza la llamada a la función auxiliar convierte() y se efectúa el paso de parámetros, a la referencia primaveras (el parámetro formal int primaveras[]) se le asocia la dirección del array edad[], la cual está representada por su propio nombre o identificador (el parámetro real edad), según se ha explicado en el apartado anterior.

Como primaveras[] es tan sólo una referencia al array edad[], la función convierte() no conoce el tamaño de este último a no ser que se le indique mediante un segundo parámetro. Esta práctica es bastante frecuente, puesto que si una función tiene que modificar un array, debe conocer su tamaño. Éste se utilizará a modo de límite superior para los valores que puede tomar el índice del array, con objeto de impedir la lectura o la escritura en posiciones de memoria que ya no pertenecen al mismo, sino que pueden corresponder a otros datos importantes del programa. Esto mismo puede aplicarse a los arrays multidimensionales, los cuales se manejan mediante el uso de varios índices y se estudiarán en el siguiente apartado.

El lenguaje C no permite el paso de arrays como parámetros por valor. Por lo tanto, una función que acepte un array como parámetro siempre podrá modificar sus valores. De este modo, queda bajo la resposabilidad del programador que la función altere los datos del array sólo si es necesario.

Ejercicio: Modificar el ejemplo 4.5 para que acepte cinco enteros suministrados por el usuario a través del teclado y posteriormente los presente en pantalla. De este modo, la función principal utilizará dos funciones auxiliares: La función datos() asignará al array los enteros proporcionados por el usuario y la función imprime() mostrará por pantalla el contenido del array, antes de ser cargado con los datos pedidos por teclado, de modo que todos sus elementos valdrán cero, y tras su carga.


Arrays multidimensionales.

A los elementos de un array multidimensional se accede en función de varios índices. Según del número de índices que se utilicen los arrays pueden ser bidimensionales (tablas), tridimensionales (cubos) y multidimensionales en general.

Ejemplo 4.6. Desarrollar un programa que, a partir de la lluvia caída durante los meses correspondientes a los años 1970, 1971 y 1972, imprima la lluvia total en cada año, la media mensual en ese año y, por último, el promedio anual.

#include <stdio.h>

#define ANNOS 3
#define MESES 12

int main() {
  float lluvia[ANNOS][MESES] = {{10.2, 8.1, 6.8, 4.2, 2.1, 1.8, 0.2, 0.3, 1.1, 2.3, 6.1, 7.4},
                                {9.2, 9.8, 4.4, 3.3, 2.2, 0.8, 0.4, 0.0, 0.6, 1.7, 4.3, 5.2},
                                {6.6, 5.5, 3.8, 2.8, 1.6, 0.2, 0.0, 0.0, 0.0, 1.3, 2.6, 4.2}};
  int anno, mes;
  double subtotal, total; /* variables locales */

  puts("Año     Lluvia (cm)   Media (cm)\n");

  for (anno = 0, total = 0; anno < ANNOS; anno++, total += subtotal) {
      for (mes = 0, subtotal = 0; mes < MESES; mes++)
          subtotal += lluvia[anno][mes]; /* subtotal por año */

      printf("%d     %lf     %lf\n", 1970 + anno, subtotal, subtotal/MESES);
      /* subtotal/MESES: media mensual por año */
      }
 

  printf("\nEl promedio anual ha sido: %lf cm.\n\n", total/ANNOS);

  system("pause");
  return 0;
}

Para inicializar el array se han incluido tres series de números entre llaves, las cuales, a su vez, están encerradas entre otro par de llaves más externas. Las reglas que controlan la situación cuando el número de datos no coincide con el tamaño del array son las mismas que en el caso de los arrays unidimensionales o vectores. Se podría haber colocado tan sólo el juego de llaves externo y prescindir de los internos, pero el código fuente queda más claro si se usan también los juegos de llaves que encierran cada una de las filas. Por tanto, las dos siguientes declaraciones son equivalentes:
int cuad[2][3] = {{1, 2, 3}, {4, 5, 6}};

int cuad[2][3] = {1, 2, 3, 4, 5, 6};

También es posible declarar arrays con un mayor número de índices. El límite está en la memoria del ordenador que se utilice.
int solido[10][20][30]; /* 6000 enteros */
Ejercicio: Modificar el programa del ejemplo 4.3 para que lea del teclado los datos de una matriz de 5x4 enteros y posteriormente los presente al usuario a través de la pantalla.

#include <stdio.h>

int main() {

  int i, j, nums[5][4];

  .
  .
  .

  system("pause");
  return 0;

}

Ejemplo 4.7. Escribir un programa en el cual se inicialice una tabla de 2x4 enteros con los números consecutivos del 1 al 8. A continuación, una función auxiliar multiplicará por dos todos los elementos del array. Finalmente, la función principal imprimirá los enteros de la tabla, ya modificados, en formato de matriz (cada fila en una línea distinta).
#include <stdio.h>

void duplica(int [][4], int, int); /* también es válido "int [2][4]" */

int main() {

  int basura[][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}};
  /* int basura[2][4] = {1, 2, 3, 4, 5, 6, 7, 8}; */


  int i, j;

  duplica(basura, 2, 4);

  for (i = 0; i < 2; i++) {
      for (j = 0; j < 4; j++)
          printf("%d ", basura[i][j]);
      putchar('\n'); /* siguiente fila en nueva línea */
      }
         
  system("pause");
  return 0;
}

void duplica(int arr[][4], int numfils, int numcols) {
  /* también es válido "int arr[2][4]" */

  int i, j;
 
  for (i = 0; i < numfils; i++)
      for (j = 0; j < numcols; j++)
          arr[i][j] *= 2; /* arr[i][j] = arr[i][j] * 2; */
}

Con la declaración del parámetro formal int arr[][4] o int arr[2][4], el compilador puede calcular la dirección efectiva del acceso al elemento arr[i][j] sumando, en primer lugar, a la dirección base de la tabla (arr) i veces el tamaño de una fila, que en este caso es el número de bytes de cuatro elementos de tipo entero: dieciséis bytes si se asume un tamaño de cuatro bytes para un entero. Una vez posicionado en la fila correspondiente, el compilador para terminar de calcular la dirección de acceso a la columna concreta añadirá a la suma anterior j veces el tamaño de un entero:

dirección_efectiva_acceso = dirección_base_tabla + i * tamaño_fila + j * tamaño_columna

Sin embargo, sería incorrecto declarar el parámetro formal sin ninguno de los tamaños (int arr[][]) o sólo con el número de filas (int arr[2][]
), porque en ninguno de los dos casos la función puede determinar el tamaño de una fila, que es el número de columnas por cada fila de la tabla. Lo único que la función conocería sería el tamaño de una columna, que es el número de bytes de un entero en este caso concreto.

Ejercicio: Completar y ejecutar el siguiente programa, el cual construye y muestra en pantalla las tablas de multiplicar del uno al nueve. Para ello, en la función principal se declarará una tabla de nueve por diez enteros para soportar las nueve tablas de multiplicar. Una función auxiliar asignará a cada elemento de la tabla el valor correspondiente. Finalmente, la función principal imprimirá el contenido de dicha tabla una vez que haya sido inicializada por la función auxiliar.
#include <stdio.h>

void creatabla(int [][10], int, int); /* también es válido "int [9][10]" */

int main() {

  int tabla[9][10]; /* tablas de multiplicar del uno al nueve */
 
...

  ...

  for (i = 0; i < 9; i++) {
      for (j = 0; j < 10; j++)
          printf("%d por %d = %d.\n", i + 1, j + 1, tabla[i][j]);
      printf("Pulse una tecla...\n\n");
      getch(); /* similar a "getchar()" pero sin pulsar <ENTER> */
      }
         
  ...
}

void creatabla(int arr[][10], int numfils, int numcols) {
  /* también es válido "int arr[9][10]" */

  ...

  ...
      ...
          arr[i][j] = (i + 1) * (j + 1);
}

Ejemplo 4.8. Realizar un programa que haga uso de una función auxiliar para ordenar ascendentemente un vector de diez enteros mediante el método de la burbuja. Una vez ordenado el vector, la función principal mostrará sus datos en pantalla. Para ordenar un vector mediante el método de la burbuja, en primer lugar es necesario comprobar si de entrada ya está ordenado, de modo que cada uno de sus elementos tenga un valor menor o igual que el siguiente. Si dos elementos consecutivos no cumplen el criterio de ordenación indicado anteriormente, se deberán intercambiar sus valores. En el caso de que se haya tenido que efectuar al menos un intercambio, una vez que finalice la comprobación del vector se deberá iniciar una nueva y proceder del modo descrito. El ciclo de comprobaciones finalizará cuando el vector esté totalmente ordenado.

Ejemplo: Ordenación ascendente mediante el método de la burbuja de un vector de cinco enteros inicialmente ordenado descendentemente. Es necesario recorrer el vector por quinta vez, después de la última modificación (el intercambio de los dos primeros elementos del vector en la cuarta iteración), para comprobar que ha quedado totalmente ordenado.
INICIO     1ª    2ª    3ª    4ª    5ª
    4          2     0    -1    -3    -3
    2          0    -1    -3    -1    -1
    0         -1    -3     0     0     0
   -1         -3     2     2     2     2
   -3          4     4     4     4     4

#include <stdio.h>

#define DIM 10


void burbuja(int [], int);
/* encabezamiento o prototipo */

i
nt main() {
  int entero[DIM], i;

  printf("Carga del vector de %d enteros:\n\n", DIM);

  for (i = 0; i < DIM; i++) {
      printf("- Elemento %d: ", i);
      scanf("%d", &entero[i]);
      }

  burbuja(entero, DIM); /* parámetros reales */

  printf("\nVector de %d enteros ordenado:\n", DIM);

  for (i = 0; i < DIM; i++)
      printf("\n- Elemento %d: %d.", i, entero[i]);

  printf("\n\n"); /* dos saltos de línea */

  system("pause");

  return 0;
}

void burbuja(int vector[], int n) { /* parámetros formales */

  int i, w, aux;

  do {
       w = 0;
       for (i = 0; i < n - 1; i++) /* comprobación del orden */
           if (vector[i] > vector[i + 1]) { /* dos elementos desordenados */
              aux = vector[i]; /* intercambio de elementos */
              vector[i] = vector[i + 1];
              vector[i + 1] = aux;
              w = 1; /* hay que volver a comprobar el vector */
              }
     } while(w); /* while(w == 1); */

}


Ejercicios de afianzamiento.

La siguiente figura muestra un modelo de funcionamiento de una neurona del cerebro humano. Ésta puede activar o no su salida J en función de un vector binario de entradas X, un vector de pesos W y un umbral §.



El vector de entradas X se multiplica (producto escalar) por el de pesos W. Si el resultado supera el umbra
l § la neurona se activa. Desarrollar una función que devuelva uno (1) si una neurona se activa y cero (0) en caso contrario. Asimismo, codificar una función principal, la cual solicite por teclado valores para el vector de entradas, el de pesos y el umbral, invoque a la función anterior y presente en pantalla el resultado devuelto por ésta. A continuación, se sugiere el siguiente prototipo para la función auxiliar:

int Neurona_activa(int X[5], int W[5], int umbral);

Escribir un programa para construir una tabla que muestre los resultados de aplicar la ley de Ohm para corriente continua (V = I * R). Dicho programa tomará valores de resistencia en incrementos de 10 Ω, a partir de un valor inicial 10 Ω y hasta un valor final de 150 Ω, y admitirá como entrada un valor de intensidad de corriente expresado en miliamperios. A continuación, pasará el valor anterior a amperios, calculará los correspondientes valores de tensión y los asignará de forma consecutiva a los elementos de un vector de 15 enteros. Finalmente, el programa recorrerá el vector para presentar en pantalla los resultados obtenidos.

Desarrollar un programa que permita realizar operaciones con números complejos. Cada complejo se representará como un vector de dimensión 2 y de tipo double. En la función principal se declararán varios vectores que soportarán los componentes (parte real y parte imaginaria) de cada uno de los números complejos. Los datos de los complejos operandos se solicitarán por teclado. Cinco funciones auxiliares codificarán las siguientes operaciones entre complejos:
void escalar(double a[], double b[], double num). Asigna al parámetro de salida b el resultado de multiplicar el complejo a por el escalar num.
void suma(double a[], double b[], double c[]). Suma los complejos a y b, y asigna el resultado al parámetro de salida c.
double modulo(double a[]). Devuelve como resultado el módulo del complejo a.
void producto(double a[], double b[], double c[]). Multiplica los complejos a y b, y asigna el resultado al parámetro de salida c.
void vector(double mod, double arg, double a[]). Asigna al parámetro de salida a las componentes vectoriales del complejo expresado en forma polar mediante el módulo mod y el argumento arg.
Para obtener la raíz cuadrada de un número real y el seno y el coseno de un ángulo pueden utilizarse respectivamente las siguientes funciones, cuyos prototipos se encuentan en el fichero de encabezamiento <math.h>:

double sqrt(double num)
double sin(double ang)
double cos(double ang)

Realizar un programa que permita operar con vectores de tres componentes reales (double) en un espacio euclídeo. Las operaciones a realizar son: suma, resta y producto escalar de dos vectores, y módulo de un vector. Se pide desarrollar una función principal en la cual se declaren tres vectores y se inicialicen, ya sea directamente o mediante valores aceptados desde el teclado, los dos primeros. Dicho programa se servirá de cuatro funciones auxiliares para llevar a cabo cada una de las operaciones indicadas. Las dos primeras funciones tienen como parámetros de entrada dos vectores y como parámetro de salida un vector al cual se asignará, respectivamente, la suma o la resta de los dos vectores anteriores. La función modulo() acepta como parámetro de entrada un vector y devuelve como resultado o valor de retorno el módulo del mismo, que será un dato de tipo double. Para la realización del producto escalar, la función correspondiente debe aceptar dos vectores como parámetros de entrada y devolver un resultado de tipo double con el valor de dicho producto. Finalmente, la función principal mostrará en pantalla el resultado de cada una de las operaciones anteriores, que en cada caso será un vector o un valor escalar.

Escribir un programa que indique mediante un mensaje en la pantalla si una matriz real de orden 3x3, inicializada con un conjunto de valores solicitados por teclado, es o no simétrica. Una matriz es simétrica si coincide con su traspuesta (Aij = Aji). Dicho programa hará uso de las funciones auxiliares carga() y simetrica() para inicializar la matriz y determinar si ésta es simétrica respectivamente. La primera de éstas tendrá como parámetro de salida una matriz 3x3. La segunda aceptará como parámetro de entrada una matriz 3x3 y devolverá como resultado a la función principal uno (1) si la matriz es simétrica y cero (0) si no lo es.

Desarrollar un programa que a partir de dos matrices reales de orden 5x5 muestre por pantalla las matrices suma y producto. La función principal utilizará tres funciones auxiliares: La primera de ellas inicializará cada una de las dos matrices operandos con valores solicitados por teclado, la segunda función calculará la matriz suma y la tercera función, la matriz producto. Una vez obtenidas las matrices anteriores, la función principal las presentará al usuario.