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 .