INTRODUCCIÓN
Hasta el momento hemos visto cómo se pueden guardar datos en
variables, arrays o cadenas de caracteres. En una variable se guarda un tipo de
dato y en los arrays, por ejemplo, se guardan muchos datos del mismo tipo. De
este tipo a menudo superamos los problemas que se nos puedan presentar a la
hora de procesar distintos tipos de datos. Pero hay ocasionas en las cuales,
utilizando el mismo nombre, deseamos almacenar información que contenga tipos
de datos diferentes. En estos casos las formas de guardar información serian
variables, arrays, cadenas, no son suficientes.
Para poder hacer frente a este problema el lenguaje C nos
ofrece un tipo de dato especial: la estructura.
Una estructura es un tipo de dato especial utilizada para
guardar información que contiene diferentes tipos de datos. La utilización de
estructuras en el lenguaje C requiere el uso de la palabra clave struct.
ESTRUCTURAS
Una estructura es un tipo de dato compuesto que permite
almacenar un conjunto de datos de diferente tipo. Los datos que contiene una
estructura pueden ser de tipo simple (caracteres, números enteros o de coma
flotante etc.) o a su vez de tipo compuesto (vectores, estructuras, listas,
etc.).
A cada uno de los
datos o elementos almacenados dentro de una estructura se les denomina miembros
de esa estructura y éstos pertenecerán a un tipo de dato determinado.
·
struct: es una palabra reservada de C que indica
que los elementos que vienen agrupados a continuación entre llaves componen una
estructura.
·
nombre_estructura: identifica el tipo de dato
que se describe y del cual se podrán declarar variables. Se especifica entre
corchetes para indicar su opcionalidad.
·
miembro1, miembro2,...: Son los elementos que
componen la estructura de datos, deben ser precedidos por el tipo_dato al cual
pertenecen.
Recordemos que una
estructura define un tipo de dato, no una variable, lo que significa que no
existe reserva de memoria cuando el compilador está analizando la estructura.
Posteriormente habrá que declarar variables del tipo definido por la estructura
para poder almacenar y manipular datos.
La declaración de
variables de un determinado tipo de estructura de datos se puede realizar de
dos modos:
1.
Primera: Incluir en la propia definición de la
estructura aquellas variables que se van a emplear en el programa. Esta
declaración de variables implica que el
ámbito en el que éstas son reconocidas será el mismo que el de la declaración
del tipo de dato estructura. En estos casos, y si no se van a declarar más
variables de este tipo de dato en otros lugares del programa, el
nombre_estructura es innecesario, por ello viene entre corchetes.
Ejemplo: estructura de una tarjeta bancaria, utilizando esta
primera forma:
struct {
long_int num_tarjeta;
char tipo_cuenta;
char nombre [80];
float saldo;
} cliente1, cliente2;
2.
Segunda: Definir el tipo de dato estructura con
un nombre determinado y declarar posteriormente las variables de ese tipo de
dato. Para ello la estructura se identificará con un nombre de forma
obligatoria.
Ejemplo: estructura de una tarjeta bancaria, utilizando la
segunda forma:
struct {
long_int num_tarjeta;
char tipo_cuenta;
char nombre [80];
float saldo;
}
struct tarjetas cli1,
cli2;}
Se ha señalado que, al igual que las matrices, las
estructuras pueden iniciarse, incluso en el mismo punto de su declaración, con
una lista de iniciadores entre corchetes separados por comas, uno para cada miembro de
la estructura. Por ejemplo, la sentencia:
struct
Cliente {int i; char str[20]; double d;} s = {33, "Pepe Lopez", 3.14
};
declara una estructura Cliente compuesta por un entero; un
array de 20 caracteres, y un doble.También inicia una estructura s como
perteneciente al tipo Cliente con valores concretos en cada campo.
En estructuras o uniones con duración automática, el
iniciador debe ser alguno de los siguientes:
Una lista de inicializadores constantes (también se permiten
expresiones con sizeof) Ejemplo:
struct
punto {int x; int y;} p1 = {1, 2};
Una sola expresión con una estructura de tipo
compatible. En este caso, el valor
inicial del objeto es el de la expresión.
Ejemplo:
struct punto p2 = p1;
en este caso, (suponiendo los valores anteriores) sería:
p2.x == 1 y p2.y == 2
Los miembros complejos de listas de iniciadores. Por ejemplo
matrices, pueden inicializarse con expresiones adecuadas incluidas en bloques
de corchetes anidados. Por ejemplo:
struct
Pesadas {
int tiket;
int pesos[5];
} s = { 150, {10, 45, 0, 78, 20}};
Si la lista de inicializadores contenidos entre los
corchetes { } es menor que los miembros de la estructura, el resto de los
miembros es inicializado implícitamente siguiendo las mismas reglas que los
objetos con duración estática.
Las estructuras, al igual que las matrices, almacenan sus
miembros de forma contigua. Por la misma razón, se pueden crear matrices de
estructuras e incluso estructuras de matrices (sus miembros son matrices).
Conceptualmente, estas últimas no se diferencian gran cosa de las matrices de
matrices (a no ser en la notación de acceso a sus miembros). También se puede
calcular su tamaño en bytes con la expresión sizeof, aunque a este respecto
debe tenerse en cuenta algunas posibles discrepancias respecto a los valores
"teóricos", debidas a las alineaciones internas realizadas por el
compilador, que se comentan .
Se ha dicho que los miembros de estructuras pueden ser de
cualquier tipo, incluso otras estructuras. Sin embargo, existen varias
limitaciones, sobre todo las que se opongan al principio anterior. Por ejemplo,
la declaración que sigue conduce a un error:
struct str
{int x; char * psch = "Hola";};
// Error!
La razón ya expuesta, que en la definición de un tipo no
pueden realizarse asignaciones, tiene su base en el comentario anterior. En
efecto: en "Constantes de cadena" , se indicó que ante la expresión
char* psch = "Hola";, el compilador debe crear una matriz de
caracteres "Hola\0"; almacenarlo en algún sitio, y asignar a una
variable tipo puntero a carácter psch (dirección del primer elemento de la
matriz). Esto conduciría a que no se podría garantizar que los miembros
estuvieran íntegramente en la estructura.
La expresión anterior sería admisible con una modificación
del tipo:
char* psch
= "Hola";
struct str
{int x; char* psch;};
pero ¡Atención!, con este esquema, el miembro psch de str no
tiene nada que ver con el puntero a carácter psch de la primera línea. Espacio
de nombres de estructuras ). str.psch sigue siendo un puntero a carácter
genérico.
Una alternativa es efectuar la asignación en una variable
concreta:
char * psch = "Hola";
struct str {int x; char * psch;}; // define el tipo de estructura str
struct str strA; // declara strA estructura
tipo str
strA.psch = psch; nbsp; // asigna valor
Podemos efectuar una comprobación de la asignación:
printf("%s\n",
strA.psch); // -> Hola
Hay que hacer hincapié en que el miembro strA.psch (puntero
a carácter) apunta ahora a la dirección de una zona de memoria donde está
almacenada la cadena "Hola\0", dirección que coincide con la indicada
por el puntero [2] psch. Si cambiamos el valor de psch, el del miembro no se
verá afectado, lo que podemos comprobar añadiendo estas sentencias:
char * psch
= "Lola";
printf("%s\n",
strA.psch); // -> Hola
El resultado evidencia que la cadena "Hola" sigue
existiendo en alguna posición de memoria y es accedida mediante strA.psch.
Se habría obtenido un resultado distinto si en vez de las
dos sentencias anteriores modificamos directamente la posición de memoria donde
está alojada la cadena "Hola". Lo hacemos mediante:
*psch = 'L';
*(psch+3) = 'o';
printf("%s\n",
strA.psch); // -> Lolo
Observe que el paréntesis *(psch+3) es necesario, ya que
*psch+3 es interpretado como (*psch)+3 = 'K'. Con lo que el intento de
asignación 'K' = 'o' produce un error del compilador: No es un Lvalue. Ya vimos
que el miembro a la izquierda de una asignación debe ser un Lvalue.
Una variante de la declaración anterior, señalada es la
siguiente:
struct str {int x; char * psch;}; // define el tipo de
estructura str
struct str strA = {1, "Hola"}; // declara e inicia strA
printf("%s\n", strA.psch); // salida: Hola
La situación final es análoga a la allí expuesta; el
resultado es una cadena "Hola\0" almacenada en algún sitio y un
puntero a carácter strA.psch apuntando a ella, como miembro de la estructura
strA. Podemos comprobarlo modificando el contenido de las posiciones de memoria
que alojan la cadena mediante:
*strA.psch
= 'L'; *(strA.psch+3) = 'o';
printf("%s\n",
strA.psch); // -> Lolo
Espacio de almacenamiento:
A continuación se expone un ejemplo de estructura con una
composición de miembros variada. En el comentario de cada línea se indica el
espacio de almacenamiento necesario en bytes [3] según las especificaciones
declaradas para el compilador utilizado. Representación interna y rango):
struct
general {
int x; // L2. 4 bytes
char ch; // L3. 1 byte
double db; // L4. 8 bytes
char * sg; // L5.
4 bytes
char nom[30]; // L6. 30 bytes
char * dir[]; // L7.
4 bytes
} str; // L8.
printf("M1:%3.0d\n",
sizeof(str.x)); // L9. -> M1:
4
printf("M2:%3.0d\n",
sizeof(str.ch)); // L10. ->
M2: 1
printf("M3:%3.0d\n",
sizeof(str.db)); // L11. ->
M3: 8
printf("M4:%3.0d\n",
sizeof(str.sg)); // L12. ->
M4: 4
printf("M5:%3.0d\n",
sizeof(str.nom)); // L13. -> M5: 30
printf("M6:%3.0d\n",
sizeof(str.dir[0])); // L14. -> M6: 4
printf("Total:%3.0d\n",
sizeof(str)); // L15. -> Total: 56
Las líneas 2, 3 y 4 no requieren comentario; L5 define un
puntero a carácter, ocupará 4 bytes en un compilador de 32 bits . La 6 define
una matriz de 30 caracteres, por lo que se reserva espacio para otros tantos.
Finalmente, L7 define un puntero a matriz de caracteres; este miembro ocupa un
espacio de 4 bytes en la estructura, aunque la matriz a la que apunta (exterior
a la propia estructura) sea de tamaño indefinido.
Las líneas 9 a 14 son simplemente una confirmación de que
las suposiciones teóricas son correctas, como efectivamente se comprueba. La
línea 15 es una comprobación del tamaño total de la estructura, con el
sorprendente resultado que se indica (la compilación se ha realizado con la
opción de alineación por defecto).
La razón de la discrepancia hay que buscarla en el hecho de
que, por razones de eficiencia (por ejemplo de velocidad de acceso a memoria),
el compilador, que asigna espacio de memoria en múltiplos de un cierto número
determinado de bits (8 en este caso), intenta no utilizar fracciones de
palabra, con lo que realiza determinados redondeos o alineaciones internas. En
este caso, la suma teórica: 51 x 8 = 408 bits habría supuesto el uso de 12.75
palabras de 32 bits, por lo que el compilador redondea a 14 palabras; 56 x 8 =
448 bits, 14 palabras. Hay que recordar que, aunque existan 5 bytes perdidos
(no utilizados), el almacenamiento ocupado por la estructura str sigue siendo
contiguo, y el acceso a sus miembros totalmente transparente para el
programador. Del mismo modo, si más tarde se declara una matriz de estructuras
de tipo general y se utilizan punteros, el compilador tiene automáticamente en
cuenta el verdadero tamaño ocupado por cada elemento, a fin de efectuar
correctamente los desplazamientos pertinentes.
Las variables y los arrays pueden pasarse como argumentos a
las funciones y todo ello sin problema alguno. Por lo tanto, ¿puede ocurrir lo
mismo con las estructuras? La respuesta es afirmativa.
Para pasar datos tipo struct a funciones en C++ es posible
hacerlo de dos diferentes maneras:
1) Invocando la función con el nombre del tipo creado como
struct. La función invocada recibe la dirección de la estructura y usa un alias
para referirse a ella.
2) Invocando la función con el nombre del tipo creado como
struct. La función invocada recibe como parámetro un dato del tipo creado como
struct.
Pasar estructuras a funciones es muy parecido a pasar un
arreglo.
En el siguiente ejemplo se usan los dos casos mencionados.
#include <iostream>
using namespace::std;
struct Datos
{
// Estos datos no se pueden
// inicializar
int anio;
int mes;
int dia;
};
// Prototipos de funcion
void Recibe( Datos &s);
void Imprime1( Datos &t);
void Imprime2( Datos Nacimiento);
/////////////////////////////////////////////////////////////
// MAIN
/////////////////////////////////////////////////////////////
int main()
{ // Abre main
// Declaracion de Elisa como tipo
Datos
Datos Elisa;
// Se reciben los datos de Elisa
mediante la funcion Recibe
Recibe(Elisa);
// Se imprimen los datos de Elisa
desde la funcion Imprime1
cout <<"\nLa fecha de nacimiento de Elisa desde Imprime1:
"<<endl;
Imprime1(Elisa);
// Se imprimen los datos de Elisa
desde la funcion Imprime2
cout <<"\nLa fecha de nacimiento de Elisa desde
Imprime2:" <<endl;
Imprime2(Elisa);
// Se imprimen los datos de Elisa
desde main
cout <<"\nLa fecha de nacimiento de Elisa desde main." <<endl;
cout <<Elisa.dia<<"/" <<Elisa.mes<< "/" << Elisa.anio << endl
<< endl;
return 0;
} // Cierra main
/////////////////////////////////////////////////////////////////
// RECIBE
////////////////////////////////////////////////////////////////
void Recibe( Datos &s)
{ // Abre funcion Recibe
cout << "\nIntroduzca el anio de nacimiento: " <<endl;
cin >> s.anio;
cout << "\nIntroduzca el mes de nacimiento: " <<endl;
cin >> s.mes;
cout <<"\nIntroduzca el dia de nacimiento: " <<endl;
cin >> s.dia;
} // Cierra funcion Recibe
////////////////////////////////////////////////////////////////
// IMPRIME1
////////////////////////////////////////////////////////////////
void Imprime1( Datos &t)
{ // Abre Imprime
cout
<<t.dia <<"/"<<t.mes<<"/"<<t.anio<<endl;
return;
} // Cierra Imprime
////////////////////////////////////////////////////////////////
// IMPRIME2
////////////////////////////////////////////////////////////////
void Imprime2( Datos Nacimiento)
{ // Abre
cout << Nacimiento.dia
<<"/" <<Nacimiento.mes<<"/"<< Nacimiento.anio << endl;
return;
}
CONCLUSIÓN
Una estructura contiene varios datos. La forma de definir
una estructura es haciendo uso de la palabra clave struct.
¿Para qué nos sirve?
Las estructuras nos ayudan a agrupar datos de una manera más ordenada y fácil
para nosotros. Por ejemplo, supongamos que queremos guardar el nombre, la edad
y el número de teléfono de un contacto en un programa en C/C++.