TEMA 3 : Estructuras de datos estáticas 

3.0 Introducción.

        En este tema se describirán las herramientas que proporciona
el  lenguaje  C  para  trabajar  con  tipos  y estructuras de datos,
flexibilizando de esta forma la  creación de programas por parte del
programador.


3.1 Matrices Estáticas.

        La matriz  es una estructura  de datos básica  dentro de los
lenguajes  de programación  y  conceptualmente  son identicas  a sus
homónimas matemáticas. Por tanto una  matriz es un conjunto de datos
de un tamaño definido que se encuentran consecutivos en memoria y en
la que es posible el acceso al elemento que deseemos simplemente con
indicar su posición.  La declaración de una matriz  en lenguaje C es
como sigue:

        tipo_de_dato identificador[tamaño1][tamaño2]...;

        Dónde :

        tipo_de_dato: Es el tipo de datos que contendrá la matriz.
Hasta ahora sólo conocemos los tipos básicos de datos; int, float,
double, char. Posteriormente veremos como definir nuestros propios
tipos de datos.

        identificador: Es el nombre que le damos a la variable
matriz y po el cual la referenciaremos en nuestro programa.

        [tamaño] : Indica el número de elementos de tipo
tipo_de_datos contendrá la matriz identificador. Si definimos dos
tamaños [tamaño1][tamaño2] nuestra matriz será bidimensional.

        Algunas declaraciones de matrices serían:

        /* Matriz de números reales de 10x10 */
        float   matriz[10][10];
        /* Matriz tridimensional de números enteros 20x20x10 */
        int     Tridimensional[20][20][10];

        Como ya se  supondrá el acceso a cada  elemento de la matriz
se realiza especificando su posición,  pero ésta comienza a contarse
desde el  valor 0, es  decir, la primera  matriz que hemos  definido
(matriz)  tendrá elementos  desde el  [0][0] al  [9][9]. Esto  puede
causar  algunos  mal  entendidos  cuando  se  trabaja  con  matrices
estáticas. Por ejemplo:

        a = matriz [2][1];
        /*  A toma  el valor  del elemeto  (2,1) comenzando a contar
desde 0 o del (3,2) si consideramos que el primer valor de la matriz
es el (1,1) */

        tridimensional [5][16][1] = 67;
        /*  Introduce  el  valor  67  en  la  entrada  de  la matriz
especificada */

        Las  variables   de  tipo  matriz  como   el  resto  de  las
declaraciones,   se  pueden   inicializar  en   el  momento   de  su
declaración,  ayudándose de  las llaves  ({}) para  la inclusión  de
inicializaciones múltiples.

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

        Estas líneas nos declararían  una matriz llamada "matriz" de
2x3 elementos  inicializada con los valores  indicados. Las matrices
son extremadamente  útiles para trabajar  con multitud de  problemas
matemáticos que se formulan de esta  forma o para mantener tablas de
datos a los  que se accede con frecuencia y  por tanto su referencia
tiene que  ser muy rápida.  Supongamos que estamos  desarrollando un
programa  para dibujar  objetos en   tres dimensiones  y más  que la
exactitud de  la representación (aunque  esto es muy  relativo), nos
interesa   la   velocidad.   En   la   representación   de   objetos
tridimensionales  se  hace  continua   referencia  a  las  funciones
trigonométricas seno  y coseno. El  cálculo de un  seno y un  coseno
puede  llevar   bastante  tiempo  así  que   antes  de  comenzar  la
representación calculamos todos los  senos y cosenos que necesitemos
(por ejemplo con  una resolución de 1 grado  -360 valores-) cada vez
que necesitemos uno de estos valores  accedemos a la matriz en lugar
de llamar a la función que nos lo calcula. Veamos como sería nuestro
programa  (las funciones  sin y   cos se  encuentran en  la librería
estandar math.h y sus paramétros están en radianes).

        #include <stdio.h>
        #include <math.h>

        main()
        {
        float   senos[360]; /* Almacenamos senos */
        float   cosenos[360];
        int     i;

        /* Inicializamos las matrices */
        for (i=0;i<360;i++)
                {
                        seno[i]   = sin (3.14159*i/180);
                        coseno[i] = cos (3.14159*i/180);
                }

        printf ("\nEl coseno de 30 es : %f",coseno[30]);
        printf ("\nEl seno de 30 es   : %f",seno[30]);
        }


3.2 Tipos compuestos

3.2.0 Introducción

        En  muchas ocasiones  nos interesaría  disponer de variables
compuestas  de  otras  variables  y  trabajar  con  ellas como si se
tratasen de una sola. Un ejemplo típico es una ficha de datos de una
agenda. Necesitaríamos  una variable que  nos almacenase el  nombre,
otra variable que nos almacenase la dirección, otra para el teléfono
y así  sucesivamente para  todos  los  datos que  deseemos mantener.
Podríamos disponer de una variable para  cada campo (cada una de las
informaciones que  componen nuestra ficha)  pero esto resultaría  un
tanto engorroso a la hora de su programación.

        El  lenguaje  C  dispone  de  mecanismos  para  trabajar con
variables compuestas de otras  variables con suma facilidad. Existen
dos tipos básicos: estructuras y uniones.


3.2.1 Estructuras de datos.

        Se trata de la forma más  versatil de trabajar con fichas de
información.  Veamos como  se definen  y posteriormente comentaremos
todos los aspectos relevantes de ellas.

        struct [Nombre_de_la_estructura]
                {
                        tipo1   campo1;
                        tipo2   campo2;
                        .
                        .
                        tipoN   campoN;
                } [variable];

        La palabra clave struct  define una estructura. Por tratarse
de un tipo  de datos puede utilizarse directamente  para definir una
variable. La  variable aparece entre corchetes  puesto que puede ser
omitida.  Si se  especifica una  variable, estaremos  definiendo una
variable cuyo tipo será la estructura que la precede. Si la variable
no   es  indicada   definimos  un   nuevo  tipo   de  datos  (struct
Nombre_de_la_estructura),  que podremos  utilizar posteriormente. Si
es  el  nombre  de  la  estructura  lo  que  se omite, tendremos que
especificar obligatoriamente una variable  que tendrá esa estructura
y no podremos  definir otras variables con  esa estructura sin tener
que  volver a  especificar todos   los campos.  Lo que  se encuentra
dentro de  las llaves es una  definición típica de variables  con su
tipo  y su  identificador. Todo  esto puede  parecer un poco confuso
pero lo aclararemos con unos ejemplos.

        struct punto
                {
                        float   x;
                        float   y;
                        int     color;
                } punto_de_fuga;

        Aquí estamos  definiendo una variable  llamada punto_de_fuga
cuyo tipo es una estructura de datos  formada por tres campos y a la
que  hemos  llamado  punto.  Dos  de  ellos  son  de  tipo  float  y
representan las coordenadas del punto,  el tercer valor es un entero
que indica  el color de ese  punto. En este caso  hemos definido una
variable y una estructura. Al disponer de un identificador para esta
última podemos definir nuevas variables de esta estructura.

        struct punto    origen1;
        struct punto    final1;

        Donde origen1  y final1 son  variables de tipo  struct punto
que   hemos  definido   anteriormente.  Si   en  la   definición  de
punto_de_fuga  no  se  hubiese  incluído  un  identificador  para la
estructura (en  este caso el  identificador es punto),  no podríamos
definir  nuevas  variables  con  esa  estructura  ya  que no estaría
identificada por ningún nombre.

        También podríamos  haber excluído el  nombre de la  variable
(punto_de_fuga).  En  este  caso   lo  que  definiríamos  sería  una
estructura llamada punto que pasaría  a ser un nuevo tipo disponible
por  el usuario.  Así los  tipos de  variables de  que dispondríamos
ahora serían:

        int
        float
        double
        char
        struct punto

        Por  tanto podríamos  definir cualquier  variable con  estos
tipos o incluso definir matriz de estos tipos.

        struct punto    matriz_de_puntos[30];

        Así estaríamos  definiendo una matriz de  30 elementos en la
que cada elemento es una struct punto con sus tres campos.

        Lo  que ahora  nos interesa  es saber  como referenciar esos
campos  y  acceder  o  modificar,   por  tanto  la  información  que
contienen. Esto se consigue separando  el identificador del campo de
la variable mediante un punto. Así:

        punto_de_fuga.x = 0;
        punto_de_fuga.y = 0;
        punto_de_fuga.color = 10;

        inicializa la cada uno de los campos de la variable punto de
fuga con sus valores correspondientes. Está claro que para acceder a
los  campos  necesitamos  alguna  variable  cuyo  tipo  sea  nuestra
estructura.  Si no  tenemos variable  no tenemos  información (sería
como hacer int = 6).

        En  el caso  de la  matriz tenemos  tantas variables de tipo
struct  punto como  las indicadas,   puesto que  el punto  separa el
nombre de la variable del campo al que queremos acceder, la forma de
modificar una entrada de la matriz sería:

        matriz_de_puntos[4].x = 6;

        matriz_de_puntos.x[4] = 6; /* No sería correcto */

        Esta   última  declaración   se  podría   utilizar  con  una
estructura de un tipo como:

        struct otra
                {
                        float   x[10];
                } matriz_de_puntos;

        Con lo cual  accederíamos al cuarto elemento del  campo x de
matriz_de_puntos que es una variable de tipo struct otra constituida
por una matriz de diez floats.

        Para  terminar  con  la  declaración  struct  indicar que es
posible la  declaración de estructuras anidadas,  es decir, un campo
de una estructura puede ser otra estructura.

        struct vector
                {
                float   x;
                float   y;
                float   z;
                };

        struct poligono_cuadrado
                {
                struct vector   p1;
                struct vector   p2;
                struct vector   p3;
                struct vecto    p4;
                };

        struct cubo
                {
                struct poligono_cuadrado        cara[6];
                int                             color;
                struct vector                   posicion;
                };

        struct cubo     mi_cubo;

        Hemos declarado  una variable (mi_cubo) de  tipo struct cubo
que es una estructura conteniendo un  valor entero que nos indica el
color  de  nuestro  objeto,  una  variable  de  tipo  struct  vector
(posicion) indicando  la posición del  objeto en un  espacio de tres
dimensiones (posicion  tiene tres campos  x,y,z por tratarse  de una
struct vector) y una matriz de seis elemento en la que cada elemento
es  un struct  poligono_cuadrado, el  cual está  formado por  cuadro
vectores que  indican los cuatro  vértices del cuadrado  en 3D. Para
aceder a todos los campos de esta variable necesitaríamos sentencias
del tipo.

        mi_cubo.color = 0;
        mi_cubo.posicion.x = 3;
        mi_cubo.posicion.y = 2;
        mi_cubo.posicion.z = 6;
        mi_cubo.cara[0].p1.x = 5;
        /* Ahora acedemos a la coordenada 0 del tercer polígono
de la cara 0 de mi_cubo*/
        mi_cubo.cara[0].p3.z = 6;
        ....


3.2.2 Estructuras solapadas. union

        La definición de una union es analoga a la definición de una
estructura.  La  diferencia  entre  ambas  es  que  los  campos  que
especifiquemos  en  una  union  ocupan  todos  la  misma posicion de
memoria. Cuando se  declara una union se reserva  espacio para poder
almacenar el  campo de mayor tamaño  de los declarados y  como ya se
dijo todos los campos ocupan la misma posición en la memoria. Veamos
un ejemplo.

        union ejemplo
                {
                        char            caracter;
                        int             entero;
                } mi_var;

        mi_var  es una  variable cuyo  tipo es  union ejemplo,  y el
acceso a  cada campo de  los definidos se  realiza igual que  en las
struct mediante la utilización de un punto. Hasta aquí nada nuevo lo
que sucede es que carácter y entero (los dos campos) ocupan la misma
posición de memoria. Así:

        mi_var.entero = 0; /* Como el tipo int ocupa más que el tipo
char ponemos a 0 toda la union */
        mi_var.caracter =  'A'; /* El código  ASCII de A es  65, por
tanto ahora mi_var.entero = 65 */
        mi_var.entero = 0x00f10;

        Esta última instrucción introduce un valor en hexadecimal en
la variable mi_var.entero. El código  hexadecimal se representa en C
anteponiendo  al número  los caracteres  0x. Para  comprender lo que
realiza esta instrucción veamos un poco como el ordenador representa
los número internamente.

        Todos hemos  oido alguna vez que  el ordenador sólo entiende
ceros  y unos,  pues bien,  lo único  que significa  ésto es  que el
ordenador cuenta en  base dos en lugar de hacerlo  en base diez como
nosotros. Cuando contamos en base diez comenzamos en 0 y al llegar a
nueve añadimos una unidad a la izquierda para indicar que llegamos a
las centenas y así consecutivamente. Cada cifra de un número en base
diez representa esa cifra multiplicada  por una potencia de diez que
depende de la posición del dígito. Es lo que se llama descomposición
factorial de un número.

        63452 = 6*10^4+3*10^3+4*10^2+5*10^1+2*10^0=
                 = 60000+3000+400+50+2

        Como nuestro  ordenador en lugar  de contar de  diez en diez
cuenta de dos  en dos cada cifra es una  potencia de dos. El sistema
de numeración en base dos se denomina sistema binario.

        b100101 = 1*2^5+0*2^4+0*2^3+1*2^2+0*2^1+1*2^0=
                = 32 + 0 + 0 + 4 + 1 = 37

        Así  es como  representa el   ordenador el  número 37  en su
sistema  binario. Cada  una de  las cifras  de un  número binario se
denomina BIT (BInary digiT) y  los ordenadores los suelen agrupar el
grupos de 8. Así 8 bits se denomina un byte, 16bits serían 2 bytes y
se denomina word o palabra y así sucesivamente.

        El mayor  número que podríamos representar  en binario con 1
byte (8bits) sería:

        b11111111 = 255

        Este es el tamaño que el lenguaje C asigna al tipo char, que
sólo puede representar 256 valores distintos, desde 0 a 255. El tipo
int short  suele ocupar una  palabra es decir,  16 bits. Así  con 16
bits el mayor número que podemos representar es:

        b1111111111111111 = 65535

      NOTA:  El  tamaño  asociado  a  cada  tipo  de  datos  es  muy
específico de  cada compilador/ordenador. No debería  darse nada por
supuesto...

        Los números  en binario rápidamente se  hacen muy largos por
ello  se  utilizan  otros  sistemas  de  numeración que permitan una
escritura  más compacta  sin perter  la información  binaria en gran
medida.  Esto sistemas  son en  general sistemas  con bases  que son
potencias de dos. Así tenemos el sistema octal (base 8) y el sistema
hexadecimal  (base 16).  Este último  es el  más ampliamente  usado,
disponemos  de  16  cifras  de  0  a  F(15)  y la característica más
importante de este sistema es que cada cifra hexadecimal, representa
cuatro bits binarios,  con lo cual el paso de  un sistema al otro es
extremadamente fácil.

        Volvamos ahora a la instrucción anteriormente indicada

        mi_var.entero = 0x00f10;

        Si pasamos este número a binario obtenemos:

        0 -> 0000
        f -> 1111 -> 15 en decimal
        1 -> 0001 ->  1 en decimal

        0f10 <-> 0000111100010000 -> 3856 en decimal

        Como  dijimos anteriormente  un char  ocupa 8  bits y un int
ocupa  16, como  la union  los solapa  tendríamos un  esquema en  la
memoria del ordenador como éste:

        int             0000111100010000        -> 3856
        char                    00010000        -> 65 ('A')

        Así mi_var.caracter contendrá el valor A, pero mi_var.entero
contendrá el valor 3856.

      NOTA:  Como  ya  se  indicó  en  la  nota  anterior, el tamaño
asignado a cada tipo depende del ordenador y del compilador. Además,
algunos ordenadores almacenan los  números en formato Bajo/Alto (los
8 bits  e  Intel)  y  otros  en  formato Alto/Bajo (Motorola, Sparc,
etc.).

        Este tipo de estructura se  suele utilizar en aplicaciones a
bajo  nivel en  la que  es  necesario  poder utilizar  este tipo  de
solapamiento  de  bits.  Como  ya  se  habrá  podido  comprobar para
compremder mínimamente  como funciona esto es  necesario bajar mucho
al  nivel  de  la  máquina  con  la  consiguiente complicación de la
explicación.


3.2.3 Tipos definidos por el usuario.

        Con  las  palabras  clave  struct  y  union, podemos definir
nuevos tipos de  variables pero tenemos que indicar  estos tipos con
todo su nombre, es decir, struct mi_struct. El lenguaje C dispone de
un comando  que nos permite  dar el nombre  que nosotros deseemos  a
cualquier  tipo de  variable. El  comando es  typedef y  su forma de
utilización es como sigue:

        typedef tipo nuevo_tipo

        Algunos ejemplos para aclarar las cosas:

        typedef unsigned char   BYTE;
        typedef struct cubo     HEXAHEDRO;

        Así con estas definiciones una declaración de las siguientes
variables:

        BYTE            var1,var2;
        HEXAEDRO        var3;

        Sería equivalente a:

        unsigned char   var1,var2;
        struct cubo     var3;