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 .
CAPITULO ANTERIOR DEL CURSO | |
PROXIMO CAPITULO DEL CURSO |