LO ÚLTIMO

ESTRUCTURAS DE AGRUPAMIENTO DE VARIABLES

 

ESTRUCTURAS DE AGRUPAMIENTO DE VARIABLES


1. CONJUNTO ORDENADO DE VARIABLES (ARRAYS)

Los arreglos ó conjuntos de datos ordenados (arrays) recolectan variables del MISMO tipo , guardandolas en forma secuencial en la memoria . La cantidad máxima de variables que pueden albergar está sólo limitada por la cantidad de memoria disponible . El tipo de las variables involucradas puede ser cualquiera de los ya vistos , con la única restricción de que todos los componentes de un array deben ser del mismo tipo .
La declaración de un array se realiza según la siguiente sintaxis :

tipo de las variables   nombre[ cantidad de elementos] ;
Por ejemplo :
int var1[10] ;
char nombre[50] ;
float numeros[200] ;
long double cantidades[25] ;

Si tomamos el primer caso , estamos declarando un array de 10 variables enteras , cada una de ellas quedará individualizada por el subíndice que sigue al nombre del mismo es decir :

var1[0] , var1[1] , etc , hasta var1[9] .

Nótese que la CANTIDAD de elementos es 10 , pero su numeración vá de 0 a 9 , y nó de 1 a 10 . En resumen un array de N elementos tiene subíndices válidos entre 0 y N - 1 . Cualquier otro número usado como subíndice , traerá datos de otras zonas de memoria , cuyo contenido es impredictible .
Se puede referenciar a cada elemento , en forma individual , tal como se ha hecho con las variables anteriormente , por ejemplo :

var1[5] = 40 ;
contador = var1[3] + 7 ;
if(var1[0] >>= 37)
..................

Tambien es posible utilizar como subíndice expresiones aritméticas , valores enteros retornados por funciones , etc . Así podríamos escribir :

printf(" %d " , var1[ ++i] ) ;
var1[8] = var1[ i + j ] ;
...............................
int una_funcion(void) ;
var1[0] = var1[ una_funcion() ] * 15 ;

Por supuesto los subíndices resultantes de las operaciones tienen que estar acotados a aquellos para los que el array fué declarado y ser enteros .
La inicialización de los arrays sigue las mismas reglas que vimos para los otros tipos de variables , es decir : Si se declaran como globales ( afuera del cuerpo de todas las funciones ) cada uno de sus elementos será automaticamente inicializado a cero . Si en cambio , su declaracion es local a una función , no se realiza ninguna inicialización , quedando a cargo del programa cargar los valores de inicio .
La inicialización de un array local , puede realizarse en su declaración , dando una lista de valores iniciales:

int numero[8] = { 4 , 7 , 0 , 0 , 0 , 9 , 8 , 7 } ;

Obsérvese que la lista está delimitada por llaves . Otra posibilidad , sólo válida cuando se inicializan todos los elementos del array , es escribir :

int  numero[] = { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 } ;

donde se obvia la declaración de la cantidad de elementos , ya que está implícita en la lista de valores constantes .
También se puede inicializar parcialmente un array , por ejemplo :

int numero[10] = { 1 , 1 , 1 } ;

en éste caso los tres primeros elementos del mismo valdran 1 , y los restantes cero en el caso que la declaración sea global , ó cualquier valor impredecible en el caso de que sea local.

2. CONJUNTO ORDENADO DE CARACTERES (STRINGS)

Los strings son simplementes arrays de caracteres , tal como los vimos hasta ahora , con el agregado de un último elemento constante : el caracter NULL ( ASCII == 0 , simbolizado por la secuencia de escape \0 ) . Este agregado permite a las funciones que procesan a los mismos , determinar facilmente la finalización de los datos .
Podemos generar un string , declarando :

char car_str[] = { 'A' , 'B' , 'C' , 'D' , 0 } ;
char car_str[] = { 'A' , 'B' , 'C' , 'D' , '\0' } ;

Ambas maneras son equivalentes. Sin embargo hay , en el lenguaje C , una forma más compacta de declararlos :

char car_str[] = "ABCD" ;
char car_str[5] = "ABCD" ;
int texto[] = "renglon 1 \n renglon 2 \n " ; /* ERROR */
unsigned char texto[] = "renglon 1 \n renglon 2 \n " ;

Simplemente en la declaración del mismo se encierran los caracteres que lo componen entre comillas . Obsérvese que en la segunda declaración , se ha explicitado ( no es necesario ) , la cantidad de elementos que tiene el string , y és uno más que la cantidad de caracteres con que se lo inicializa , para dejar lugar al NULL . Todas éstas declaraciones agregan automáticamente el NULL como último elemento del array .
Un caso interesante es él de la tercer línea ( comentada como ERROR ) , con el fín de poder albergar al caracter "\n"20( ASCII 179 ) se intentó asignar el string a un array de enteros , Esto no es permitido por el compilador , que lo rechaza como una asignación inválida . La razón de ello se verá más adelante cuando analicemos punteros , ya que el string constante usado como rvalue es un puntero a char , y no a int . La solución mas común para este caso es , declarar el array como unsigned char , con lo que llevamos el alcance de sus elementos a 255 . Si tuvieramos el caso de tener que albergar en un string el caracter EOF ( -1 ) y al mismo tiempo caracteres con ASCII mayor que 127 ,se podría definir el array como int , pero su inicialización se tendrá que hacer obligatoriamente usando llaves , como vimos anteriormente .
Se deduce entonces , de lo antedicho que un string sigue siendo un array de caracteres , con la salvedad del agregado de un terminador , por lo que las propiedades que veremos a continuacion , se aplicaran indistintamente a ambos.

3. ARRAYS Y STRINGS COMO ARGUMENTOS DE FUNCIONES

Los arrays , como todos los otros tipos de variables , pueden ser pasados como argumentos a las funciones . Veamos esquematicamente como sería la sintaxis :


double funcion_1( float numeros[10] , char palabra[] ) ; /*linea 1*/
.......................................................
main() /*linea 2*/
{
float numeros[10] = { 1.1 , 2.2 , 3.0 } ; /*linea 3*/
char palabra[] = " Lenguaje C " ; /*linea 4*/
double c ; /*linea 5*/
........................................................
c = funcion_1( numeros , palabra ) /*linea 6*/
........................................................
}
double funcion_1( float numeros[10] , char palabra[] ) /*linea 7*/
{
........................................................
}

Es necesario analizar con mucho detenimiento , este último ejemplo . En la primer línea declaramos el prototipo de funcion_1() que recibe como argumentos dos arrays , uno de 10 elementos del tipo float , y otro de caracteres de longitud indeterminada . En el primer caso la función necesitará saber de alguna manera cual es la longitud del array numérico recibido, mientras que en el segundo , no hace falta , ya que la función puede ser construída para que , por sí misma , detecte la finalización del string por la presencia del caracter NULL . Se podría generalizar más el programa declarando :

double funcion_1( double numeros[] , int longitud_array , char palabra[] ) ;

en donde , en la variable longitud_array se enviaría la cantidad de elementos de numero[] .
En la tercer línea se declara el array numérico , inicializandose sólo los tres primeros elementos , y en la cuarta línea se declara el string .
En la séptima línea se dá la definición de la función , de acuerdo al prototipo escrito anteriormente .
Si miramos con detenimiento la sexta línea , el llamado a la función , vemos que los argumentos pasados sólo tienen el NOMBRE de ambos arrays . Esta es la diferencia más importante entre este tipo de estructura de datos y las variables simples vistas anteriormente , ya que los arrays son pasados a las funciones por DIRECCION y nó por valor .
En el lenguaje C se prefiere , para evitar el uso abusivo del stack , cuando hay que enviar a una función una larga estructura de datos , en lugar de copiar a todos ellos , cargar el stack sólo con la dirección de la posición de memoria donde está ubicado el primero de los mismos.
El nombre de un array equivale sintácticamente a la direccion del elemento cero así será :

numero  ==  dirección de numero[0]
palabra == direccion de palabra[0]


Esto habilita a las funciones a que puedan acceder a los arrays directamente , allí donde el programa los ha ubicado en la memoria , por lo que pueden MODIFICARLOS EN FORMA PERMANENTE aunque no hayan sido declarados como locales a la función misma ní globales al programa .
Es muy importante recordar este último concepto , a fín de evitar errores muy comunes , en los primeros intentos de programación en C .
Otra característica importante de los arrays es que , su nombre ( ó dirección del primer elemento ) es una CONSTANTE y nó una variable . El nombre de los arrays implican para el compilador el lugar de memoria donde empieza la estructura de datos por lo que , intentar cambiar su valor es tomado como un error , asI si escribieramos por ejemplo :


char titulo[] = "Primer titulo" ;
....................................
titulo = "subtitulo" ;

La primer sentencia es correcta , ya que estamos incializando al string , pero la segunda produciría un error del tipo " LVALUE REQUERIDO " , es decir que el compilador espera ver , del lado izquierdo de una expresión , a una variable y en cambio se ha encontrado con una constante titulo (ó sea la dirección de memoria donde está almacenada la P de "Primer título") . Esto al compilador le suena similar a una expresión de la clase : 124 = j y se niega rotundamente a compilarla.

4. ARRAYS MULTIDIMENSIONALES.

Las estructuras de datos del tipo array pueden tener más de una dimensión , es bastante común el uso de arrays "planos" ó matriciales de dos dimensiones , por ejemplo :


int matriz[ número total de filas ] [ número total de columnas ] ;

Si declaramos :

int matriz[3][4] ;


esquematicamente la disposicion "espacial" de los elementos seria:



columnas: 0 1 2 3

filas 0 [0][0] [0][1] [0][2] [0][3] matriz[0][]

1 [1][0] [1][1] [1][2] [1][3] matriz[1][]

2 [2][0] [2][1] [2][2] [2][3] matriz[2][]

Por supuesto , aunque menos usados , se pueden generar arrays de cualquier número de dimensiones .
Para inicializar arrays multidimensionales , se aplica una técnica muy similar a la ya vista , por ejemplo para dar valores iniciales a un array de caracteres de dos dimensiones , se escribirá :

char dia_de_la_semana[7][8] = {
"lunes" , "martes" , " miercoles" ,
"jueves" , "viernes" , "sábado" ,
"domingo"
} ;

Acá el elemento [0][0] será la "l" de lunes , el [2][3] la "r" de miercoles , el [5][2] la "b" de sabado, etc. Nótese que los elementos [0][5] , [1][6] ,etc estan inicializados con el caracter NULL y demas [0][6] y [0][7], etc no han sido inicializados. Si le parece que en este párrafo se nos escapó un error , está equivocado , lo que ocurre es que se olvidó de contar los índices desde 0.
Este último ejemplo también podría verse como un array unidimensional de strings.

5. ESTRUCTURAS

DECLARACION DE ESTRUCTURAS

Así como los arrays son organizaciones secuenciales de variables simples , de un mismo tipo cualquiera dado , resulta necesario en multiples aplicaciones , agrupar variables de distintos tipos , en una sola entidad . Este sería el caso , si quisieramos generar la variable " legajo personal " , en ella tendriamos que incluir variables del tipo : strings , para el nombre , apellido , nombre de la calle en donde vive , etc , enteros , para la edad , número de codigo postal , float ( ó double , si tiene la suerte de ganar mucho ) para el sueldo , y así siguiendo . Existe en C en tipo de variable compuesta , para manejar ésta situación típica de las Bases de Datos , llamada ESTRUCTURA . No hay limitaciones en el tipo ni cantidad de variables que pueda contener una estructura , mientras su máquina posea memoria suficiente como para alojarla , con una sóla salvedad : una estructura no puede contenerse a sí misma como miembro .
Para usarlas , se deben seguir dos pasos . Hay que , primero declarar la estructura en sí , ésto es , darle un nombre y describir a sus miembros , para finalmente declarar a una ó más variables , del tipo de la estructura antedicha , veamos un ejemplo :

struct legajo {
int edad ;
char nombre[50] ;
float sueldo ;
} ;
struct legajo legajos_vendedores , legajos_profesionales ;

En la primer sentencia se crea un tipo de estructura , mediante el declarador "struct",luego se le dá un nombre " legajo " y finalmente , entre llaves se declaran cada uno de sus miembros , pudiendo estos ser de cualquier tipo de variable , incluyendo a los arrays ó aún otra estructura . La única restricción es que no haya dos miembros con el mismo nombre , aunque sí pueden coincidir con el nombre de otra variable simple , ( o de un miembro de otra estructura ) , declaradas en otro lugar del programa. Esta sentencia es sólo una declaración , es decir que no asigna lugar en la memoria para la estructura , sólo le avisa al compilador como tendrá que manejar a dicha memoria para alojar variables del tipo struct legajo .
En la segunda sentencia , se definen dos variables del tipo de la estructura anterior ,(ésta definición debe colocarse luego de la declaración ) , y se reserva memoria para ambas .
Las dos sentencias pueden combinarse en una sola , dando la definición a continuación de la declaracion :

struct legajo {
int edad ;
char nombre[50] ;
float sueldo ;
} legajo_vendedor , legajo_programador ;

Y si nó fueran a realizarse más declaraciones de variables de éste tipo , podría obviarse el nombre de la estructura ( legajo ).

Las variables del tipo de una estructura , pueden ser inicializadas en su definición , así por ejemplo se podría escribir:

struct legajo {
int edad ;
char nombre[50] ;
float sueldo ;
char observaciones[500] ;
} legajo_vendedor = { 40 , "Juan Eneene" , 1200.50 ,
"Asignado a zona A" } ;
struct legajo  legajo_programador = { 23 , "Jose Peres" , 2000.0 ,
"Asignado a zona B" } ;

Acá se utilizaron las dos modalidades de definición de variables , inicializandolas a ambas.

REGLAS PARA EL USO DE ESTRUCTURAS

Lo primero que debemos estudiar es el método para dirigirnos a un miembro particular de una estructura .Para ello existe un operador que relaciona al nombre de ella con el de un miembro , este operador se representa con el punto ( . ) , así se podrá referenciar a cada uno de los miembros como variables individuales , con las particularidades que les otorgan sus propias declaraciones , internas a la estructura.
La sintaxis para realizar ésta referencia es : nombre_de_la_estructura.nombre_del_miembro , así podremos escribir por ejemplo , las siguientes sentencias

strut posicion_de {
float eje_x ;
float eje_y ;
float eje_z ;
} fin_recta , inicio_recta = { 1.0 , 2.0 , 3.0 ) ;

fin_recta.eje_x = 10.0 ;
fin_recta.eje_y = 50.0 ;
fin_recta.eje_z = 90.0 ;

if( fin_recta.eje_x == inicio_recta.eje_x )
..........................................

Es muy importante recalcar que , dos estructuras , aunque sean del mismo tipo , no pueden ser asignadas ó comparadas la una con la otra , en forma directa , sino asignando ó comparandolas miembro a miembro. Esto se vé claramente explicitado en las líneas siguientes , basadas en las declaraciones anteriores:

fin_recta = inicio_recta ;             /* ERROR */
if( fin_recta >>= inicio_recta ); /* ERROR */
fin_recta.eje_x = inicio_recta.eje_x ; /* FORMA CORRECTA DE ASIGNAR */
fin_recta.eje_y = inicio_recta.eje_y ; /* UNA ESTRUCTURA A OTRA */
fin_recta.eje_z = inicio_recta.eje_z ;
if( (fin_recta.eje_x >>= inicio_recta.eje_x) &&     /* FORMA CORRECTA DE  */
(fin_recta.eje_y >>= inicio_recta.eje_y) && /* COMPARAR UNA */
(fin_recta.eje_z >>= inicio_recta.eje_z) ) /* ESTRUCTURA CON OTRA */

Las estructuras pueden anidarse , es decir que una ó mas de ellas pueden ser miembro de otra . Las estructuras también pueden ser pasadas a las funciones como parámetros , y ser retornadas por éstas , como resultados.

6. ARRAYS DE ESTRUCTURAS

Cuando hablamos de arrays dijimos que se podían agrupar , para formarlos , cualquier tipo de variables , esto es extensible a las estructuras y podemos entonces agruparlas ordenadamente , como elementos de un array . Veamos un ejemplo :

typedef struct {
char material[50] ;
int existencia ;
double costo_unitario ;
} Item ;
Item  stock[100] ;

Hemos definido aquí un array de 100 elementos , donde cada uno de ellos es una estructura del tipo Item compuesta por tres variables , un int , un double y un string ó array de 50 caracteres.

Los arrays de estructuras pueden inicializarse de la manera habitual , así en una definición de stock, podríamos haber escrito:

Item   stock1[100] = {
"tornillos" , 120 , .15 ,
"tuercas" , 200 , .09 ,
"arandelas" , 90 , .01
} ;
Item stock2[] = {
{ 'i','t','e','m','1','\0' } , 10 , 1.5 ,
{ 'i','t','e','m','2','\0' } , 20 , 1.0 ,
{ 'i','t','e','m','3','\0' } , 60 , 2.5 ,
{ 'i','t','e','m','4','\0' } , 40 , 4.6 ,
{ 'i','t','e','m','5','\0' } , 10 , 1.2 ,
} ;

Analicemos un poco las diferencias entre la dos inicializaciones dadas , en la primera , el array material[] es inicializado como un string , por medio de las comillas y luego en forma ordenada , se van inicializando cada uno de los miembros de los elementos del array stock1[] , en la segunda se ha preferido dar valores individuales a cada uno de los elementos del array material , por lo que es necesario encerrarlos entre llaves .
Sin embargo hay una diferencia mucho mayor entre las dos sentencias , en la primera explicitamos el tamaño del array , [100] , y sólo inicializamos los tres primeros elementos , los restantes quedarán cargados de basura si la definición es local a alguna función , ó de cero si es global , pero de cualquier manera están alojados en la memoria , en cambio en la segunda dejamos implícito el número de elementos , por lo que será el compilador el que calcule la cantidad de ellos , basandose en cuantos se han inicializado , por lo tanto este array sólo tendrá ubicados en memoria cuatro elementos , sin posibilidad de agregar nuevos datos posteriomente .
Veremos más adelante que en muchos casos es usual realizar un alojamiento dinámico de las estructuras en la memoria , en razón de ello , y para evitar ademas el saturación de stack por el pasaje ó retorno desde funciones , es necesario conocer el tamaño , ó espacio en bytes ocupados por ella .
Podemos aplicar el operador sizeof , de la siguiente manera :

longitud_base_de_datos  = sizeof( stock1 ) ;
longitud_de_dato = sizeof( Item ) ;
cantidad_de_datos = sizeof( stock1 ) / sizeof( Item ) ;

Con la primera calculamos el tamaño necesario de memoria para albergar a todos datos, en la segunda la longitud de un sólo elemento ( record ) y por supuesto dividiendo ambas , se obtiene la cantidad de records.

7. UNIONES

Las uniones son a primera vista, entidades muy similares a las estructuras, están formadas por un número cualquiera de miembros, al igual que aquellas, pero en éste caso no existen simultaneamente todos los miembros, y sólo uno de ellos tendrá un valor válido.
Supongamos por caso, que queremos guardar datos para un stock de materiales , pero los mismos pueden ser identificados , en un caso con el número de articulo (un entero ) y en otro por su nombre ( un string de 10 letras como máximo ). No tendría sentido definir dos variables , un int y un string , para cada artículo , ya que voy a usar una modalidad ú la otra, pero nó las dos simultaneamente. Las uniones resuelven este caso , ya que si declaro una que contenga dos miembros, un entero y un string , sólo se reservará lugar para el mayor de ellos , en estee caso, el string , de tal forma que si asigno un valor a éste se llenará ese lugar de la memoria con los caracteres correspondientes, pero si en cambio asigno un valor al miembro declarado como int éste se guardará en los dos primeros bytes del MISMO lugar de memoria. Por supuesto, en una unión, sólo uno de los miembros tendrá entonces un valor correcto .