Informatica
  Tipos de variables III: Estructuras
 

11 Tipos de variables III: Estructuras

Las estructuras son el segundo tipo de datos estructurados que veremos.

Las estructuras nos permiten agrupar varios datos, aunque sean de distinto tipo, que mantengan algún tipo de relación, permitiendo manipularlos todos juntos, con un mismo identificador, o por separado.

Las estructuras son llamadas también muy a menudo registros, o en inglés "records". Y son estructuras análogas en muchos aspectos a los registros de bases de datos. Y siguiendo la misma analogía, cada variable de una estructura se denomina a menudo campo, o "field".

Sintaxis:

struct [<identificador>] {
   [<tipo> <nombre_variable>
[,<nombre_variable>,...]]; . } [<variable_estructura>
[,<variable_estructura>,...];

El nombre de la estructura es un nombre opcional para referirse a la estructura.

Las variables de estructura son variables declaradas del tipo de la estructura, y su inclusión también es opcional. Sin bien, al menos uno de estos elementos debe existir, aunque ambos sean opcionales.

En el interior de una estructura, entre las llaves, se pueden definir todos los elementos que consideremos necesarios, del mismo modo que se declaran las variables.

Las estructuras pueden referenciarse completas, usando su nombre, como hacemos con las variables que ya conocemos, y también se puede acceder a los elementos en el interior de la estructura usando el operador de selección (.), un punto.

También pueden declararse más variables del tipo de estructura en cualquier parte del programa, de la siguiente forma:

[struct] <identificador> <variable_estructura>
   [,<variable_estructura>...];

En C++ la palabra "struct" es opcional en la declaración de variables. En C es obligatorio usarla.

Ejemplo:

struct Persona {
   char Nombre[65]; 
   char Direccion[65]; 
   int AnyoNacimiento; 
} Fulanito;

Este ejemplo declara a Fulanito como una variable de tipo Persona. Para acceder al nombre de Fulanito, por ejemplo para visualizarlo, usaremos la forma:

cout << Fulanito.Nombre;

Funciones en el interior de estructuras:   

C++, al contrario que C, permite incluir funciones en el interior de las estructuras. Normalmente estas funciones tienen la misión de manipular los datos incluidos en la estructura.

Aunque esta característica se usa casi exclusivamente con las clases, como veremos más adelante, también puede usarse en las estructuras.

Dos funciones muy particulares son las de inicialización, o constructor, y el destructor. Veremos con más detalle estas funciones cuando asociemos las estructuras y los punteros.

El constructor es una función sin tipo de retorno y con el mismo nombre que la estructura. El destructor tiene la misma forma, salvo que el nombre va precedido el operador "~".

Nota: para aquellos que usen un teclado español, el símbolo "~" se obtiene pulsando las teclas del teclado numérico 1, 2, 6, mientras se mantiene pulsada la tecla ALT, ([ALT]+126). También mediante la combinación [Atl Gr]+[4] (la tecla [4] de la zona de las letras, no del teclado numérico).

Veamos un ejemplo sencillo para ilustrar el uso de constructores:

Forma 1:

struct Punto { 
   int x, y; 
   Punto() {x = 0; y = 0;} // Constructor
} Punto1, Punto2;

Forma 2:

struct Punto { 
   int x, y; 
   Punto(); // Declaración del constructor
} Punto1, Punto2;
 
// Definición del constructor, fuera de la 
estructura Punto::Punto() { x = 0; y = 0; }

Si no usáramos un constructor, los valores de x e y para Punto1 y Punto2 estarían indeterminados, contendrían la "basura" que hubiese en la memoria asignada a estas estructuras durante la ejecución. Con las estructuras éste será el caso más habitual.

Mencionar aquí, sólo a título de información, que el constructor no tiene por qué ser único. Se pueden incluir varios constructores, pero veremos esto mucho mejor y con más detalle cuando veamos las clases.

Usando constructores nos aseguramos los valores iniciales para los elementos de la estructura. Veremos que esto puede ser una gran ventaja, sobre todo cuando combinemos estructuras con punteros, en capítulos posteriores.

También podemos incluir otras funciones, que se declaran y definen como las funciones que ya conocemos, salvo que tienen restringido su ámbito al interior de la estructura.

Otro ejemplo:

#include <iostream>
using namespace std;

struct stPareja { 
   int A, B; 
   int LeeA() { return A;} 
// Devuelve el valor de A int LeeB() { return B;}
// Devuelve el valor de B void GuardaA(int n) { A = n;}
// Asigna un nuevo valor a A void GuardaB(int n) { B = n;}
// Asigna un nuevo valor a B } Par; int main() { Par.GuardaA(15); Par.GuardaB(63); cout << Par.LeeA() << endl; cout << Par.LeeB() << endl; cin.get(); return 0; }

En este ejemplo podemos ver cómo se define una estructura con dos campos enteros, y dos funciones para modificar y leer sus valores. El ejemplo es muy simple, pero las funciones de guardar valores se pueden elaborar para que no permitan determinados valores, o para que hagan algún tratamiento de los datos.

Por supuesto se pueden definir otras funciones y también constructores más elaborados y sobrecarga de operadores. Y en general, las estructuras admiten cualquiera de las características de las clases, siendo en muchos aspectos equivalentes.

Veremos estas características cuando estudiemos las clases, y recordaremos cómo aplicarlas a las estructuras.

Inicialización de estructuras:  

De un modo parecido al que se inicializan los arrays, se pueden inicializar estructuras, tan sólo hay que tener cuidado con las estructuras anidadas. Por ejemplo:

struct A {
   int i;
   int j;
   int k;
};

struct B {
   int x;
   struct C {
      char c;
      char d;
   } y;
   int z;
};

A ejemploA = {10, 20, 30};
B ejemploB = {10, {'a', 'b'}, 20};

Cada nueva estructura anidada deberá inicializarse usando la pareja correspondiente de llaves "{}", tantas veces como sea necesario.

Asignación de estructuras:  

La asignación de estructuras está permitida, pero sólo entre variables del mismo tipo de estructura, salvo que se usen constructores, y funciona como la intuición dice que debe hacerlo.

Veamos un ejemplo:

struct Punto { 
   int x, y; 
   Punto() {x = 0; y = 0;} 
} Punto1, Punto2;
 
int main() { 
   Punto1.x = 10; 
   Punto1.y = 12; 
   Punto2 = Punto1; 
}

La línea:

Punto2 = Punto1;

equivale a:

Punto2.x = Punto1.x; 
Punto2.y = Punto1.y;

Arrays de estructuras:   

La combinación de las estructuras con los arrays proporciona una potente herramienta para el almacenamiento y manipulación de datos.

Ejemplo:

struct Persona { 
   char Nombre[65]; 
   char Direccion[65]; 
   int AnyoNacimiento; 
} Plantilla[200];

Vemos en este ejemplo lo fácil que podemos declarar el array Plantilla que contiene los datos relativos a doscientas personas.

Podemos acceder a los datos de cada uno de ellos:

cout << Plantilla[43].Direccion;

O asignar los datos de un elemento de la plantilla a otro:

Plantilla[0] = Plantilla[99];

Estructuras anidadas:   

También está permitido anidar estructuras, con lo cual se pueden conseguir superestructuras muy elaboradas.

Ejemplo:

struct stDireccion { 
   char Calle[64]; 
   int Portal; 
   int Piso; 
   char Puerta[3]; 
   char CodigoPostal[6]; 
   char Poblacion[32]; 
};
 
struct stPersona { 
   struct stNombre { 
      char Nombre[32]; 
      char Apellidos[64]; 
   } NombreCompleto; 
   stDireccion Direccion; 
   char Telefono[10]; 
};
...

En general, no es una práctica corriente definir estructuras dentro de estructuras, ya que resultan tener un ámbito local, y para acceder a ellas se necesita hacer referencia a la estructura más externa.

Por ejemplo para declarar una variable del tipo stNombre hay que utilizar el operador de acceso (::):

stPersona::stNombre NombreAuxiliar;

Sin embargo para declarar una variable de tipo stDireccion basta con declararla:

stDireccion DireccionAuxiliar;

Estructuras anónimas:  

Una estructura anónima es la que carece de 
identificador de tipo de estructura y de
declaración de variables del tipo de
estructura.
Por ejemplo, veamos esta declaración:
struct stAnonima {
  struct {
    int x;
    int y;
  };
  int z;
};
Para acceder a los campos "x" o "y" se 
usa la misma forma que para el campo "z":
stAnonima Anonima;
 
   Anonima.x = 0;
   Anonima.y = 0;
   Anonima.z = 0;
Pero, ¿cual es la utilidad de esto?
La verdad, no mucha, al menos cuando se 
usa con estructuras. En el capítulo dedicado
a las uniones veremos que sí puede resultar
muy útil.
El método usado para declarar la estructura 
dentro de la estructura es la forma anónima,
como verás no tiene identificador de tipo
de estructura ni de campo. El único lugar
donde es legal el uso de estructuras
anónimas es en el interior de estructuras
y uniones.

Operador "sizeof" con
estructuras:  

Cuando se aplica el operador sizeof a una 
estructura, el tamaño obtenido no siempre
coincide con el tamaño de la suma de sus
campos. Por ejemplo:
#include <iostream>
using namespace std;
 
struct A {
   int x;
   char a;
   int y;
   char b;
};

struct B {
   int x;
   int y;
   char a;
   char b;
};

int main()
{
   cout << "Tamaño de int: " 
        << sizeof(int) << endl;
   cout << "Tamaño de char: " 
        << sizeof(char) << endl;
   cout << "Tamaño de estructura A: " 
        << sizeof(A) << endl;
   cout << "Tamaño de estructura B: " 
        << sizeof(B) << endl;

   cin.get();
   return 0;
}
El resultado, usando Dev-C++, es el 
siguiente:
Tamaño de int: 4
Tamaño de char: 1
Tamaño de estructura A: 16
Tamaño de estructura B: 12
Si hacemos las cuentas, en ambos casos el 
tamaño de la estructura debería ser el
mismo, es decir, 4+4+1+1=10 bytes. Sin
embargo en el caso de la estructura A el
tamaño es 16 y en el de la estructura B
es 12, ¿por qué?
La explicación es algo denominado alineación
de bytes (byte-aling). Para mejorar el
rendimiento del procesador no se accede a
todas las posiciones de memoria. En el
caso de microprocesadores de 32 bits
(4 bytes), es mejor si sólo se accede a
posiciones de memoria múltiplos de 4, y
el compilador intenta alinear las
variables con esas posiciones.
 
En el caso de variables "int" es fácil, 
ya que ocupan 4 bytes, pero con las
variables "char" no, ya que sólo ocupan 1.
Cuando se accede a datos de menos de 4 bytes
la alineación no es tan importante. El
rendimiento se ve afectado sobre todo cuando
hay que leer datos de cuatro bytes que no
estén alineados.
En el caso de la estructura A hemos 
intercalado campos "int" con "char",
de modo que el campo "int" "y", se alinea
a la siguiente posición múltiplo de 4,
dejando 3 posiciones libres después del
campo "a". Lo mismo pasa con el campo "b".
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
x
a
vacío
y
b
vacío
En el caso de la estructura B hemos 
agrupado los campos de tipo "char"
al final de la estructura, de modo que
se aprovecha mejor el espacio, y sólo
se desperdician los dos bytes sobrantes
después de "b".
0
1
2
3
4
5
6
7
8
9
10
11
x
y
a
b
vacío

Campos de bits:  

Existe otro tipo de estructuras que 
consiste en empaquetar los campos de
la estructura en el interior de enteros,
usando bloques o conjuntos de bits para
cada campo.
Por ejemplo, una variable char contiene 
ocho bits, de modo que dentro de ella
podremos almacenar ocho campos de un bit,
o cuatro de dos bits, o dos de tres y uno
de dos, etc. En una variable int de 16 bits
podremos almacenar 16 bits, etc.
Debemos usar siempre valores de enteros 
sin signo, ya que el signo se almacena
en un bit del entero, el de mayor peso,
y puede falsear los datos almacenados
en la estructura.
La sintaxis es:
struct [<nombre de la estructura>] {
unsigned <tipo_entero> <identificador>:
<núm_de_bits>; . } [<lista_variables>];
Hay algunas limitaciones, por ejemplo, 
un campo de bits no puede ocupar dos
variables distintas, todos sus bits
tienen que estar en el mismo valor entero.
Veamos algunos ejemplos:
struct mapaBits {
   unsigned char bit0:1;
   unsigned char bit1:1;
   unsigned char bit2:1;
   unsigned char bit3:1;
   unsigned char bit4:1;
   unsigned char bit5:1;
   unsigned char bit6:1;
   unsigned char bit7:1;
   };
 
struct mapaBits2 {
   unsigned short int campo1:3;
   unsigned short int campo2:4;
   unsigned short int campo3:2;
   unsigned short int campo4:1;
   unsigned short int campo5:6;
};
 
struct mapaBits3 {
   unsigned char campo1:5;
   unsigned char campo2:5;
};
En el primer caso se divide un valor char 
sin signo en ocho campos de un bit cada uno:
7
6
5
4
3
2
1
0
bit7
bit6
bit5
bit4
bit3
bit2
bit1
bit0
En el segundo caso dividimos un valor 
entero sin signo de dieciséis bits en
cinco campos de distintas longitudes:
15
14
13
12
11
10
9
8
7
6
5
4
3
2
1
0
campo5
campo4
campo3
campo2
campo1
Los valores del campo5 estarán limitados 
entre 0 y 63, que son los números que se
pueden codificar con seis bits. Del mismo
modo, el campo4 sólo puede valer 0 ó 1, etc.
unsigned char
unsigned char
7
6
5
4
3
2
1
0
7
6
5
4
3
2
1
0
 
 
 
campo2
 
 
 
campo1
En este ejemplo vemos que como no es 
posible empaquetar el campo2 dentro
del mismo char que el campo1, se añade
un segundo valor char, y se dejan sin
usar los bits sobrantes.
También es posible combinar campos de bits 
con campos normales, por ejemplo:
struct mapaBits2 {
   int numero;
   unsigned short int campo1:3;
   unsigned short int campo2:4;
   unsigned short int campo3:2;
   unsigned short int campo4:1;
   unsigned short int campo5:6;
   float n;
};
Los campos de bits se tratan en general 
igual que cualquier otro de los campos de
una estructura. Se les puede asignar
valores (dentro del rango que admitan),
pueden usarse en condicionales, imprimirse,
etc.
#include <iostream>
#include <cstdlib>
using namesoace std;
 
struct mapaBits2 {
   unsigned short int campo1:3;
   unsigned short int campo2:4;
   unsigned short int campo3:2;
   unsigned short int campo4:1;
   unsigned short int campo5:6;
};

int main()
{
   mapaBits2 x;
   
   x.campo2 = 12;
   x.campo4 = 1;
   cout << x.campo2 << endl;
   cout << x.campo4 << endl;
   
   cin.get();
   return 0;
}
No es normal usar estas estructuras en 
programas, salvo cuando se relacionan con
ciertos dispositivos físicos, por ejemplo,
para configurar un puerto serie en MS-DOS
se usa una estructura empaquetada en un
unsigned char, que indica los bits de
datos, de parada, la paridad, etc, es
decir, todos los parámetros del puerto.
En general, para programas que no requieran
estas estructuras, es mejor usar estructuras
normales, ya que son mucho más rápidas.
Otro motivo que puede decidirnos por estas 
estructuras es el ahorro de espacio, ya sea
en disco o en memoria. Si conocemos los
límites de los campos que queremos almacenar,
y podemos empaquetarlos en estructuras de
mapas de bits podemos ahorrar mucho espacio.

Palabras reservadas usadas
en este capítulo

struct.

Problemas:  

Escribir un programa que almacene enun 
array los nombres y números deteléfono
de 10 personas. El programadebe leer los
datos introducidos por el usuario y
guardarlos en memoria. Después debe ser
capaz de buscar el nombre correspondiente
a un número de teléfono y el teléfono
correspondiente a una persona. Ambas
opciones deben se accesibles a través de
un menú, así como la opción de salir del
programa. El menú debe tener esta forma,
más o menos:

a) Buscar por nombre
b) Buscar por número de teléfono
c) Salir

Pulsa una opción:

Nota: No olvides que para comparar cadenas 
se debe usar una función, no el operador ==.
Para almacenar fechas podemos crear
una estructura con tres campos: ano,
mes y día. Los días pueden tomar
valores entre 1 y 31, los meses de
1 a 12 y los años, dependiendo de
la aplicación, pueden requerir
distintos rangos de valores.
Para este ejemplo consideraremos
suficientes 128 años, entre 1960 y
2087. En ese caso el año se obtiene
sumando 1960 al valor de año. El año
2003 se almacena como 43.
Usando estructuras, y ajustando los
tipos de los campos, necesitamos un
char para día, un char para mes y
otro para año.
Diseñar una estructura análoga,
llamada "fecha", pero usando campos
de bits. Usar sólo un entero corto
sin signo (unsigned short), es decir,
un entero de 16 bits. Los nombres de
los campos serán: dia, mes y anno.
Basándose en la estructura de bits
del ejercicio anterior, escribir una
función para mostrar fechas: void 
Mostrar(fecha);. El formato debe
ser: "dd de mmmmmm de aaaa", donde
dd es el día, mmmmmm el mes con letras,
y aaaa el año. Usar un array para
almacenar los nombres de los meses.
Basándose en la estructura de bits
del ejercicio anterior, escribir una
función bool ValidarFecha(fecha);,
que verifique si la fecha entregada
como parámetro es válida. El mes
tiene que estar en el rango de 1 a 12,
dependiendo del mes y del año, el día
debe estar entre 1 y 28, 29, 30 ó 31.
El año siempre será válido, ya que debe
estar en el rango de 0 a 127.
Para validar los días usaremos un array
int DiasMes[] = {31, 28, 31, 30, 31, 30, 31, 31,
 30, 31, 30, 31};. Para el caso de que el
mes sea febrero, crearemos otra función
para calcular si un año es o no bisiesto:
bool Bisiesto(int); Los años bisiestos son
los divisibles entre 4, al menos en el
rango de 1960 a 2087 se cumple.
Nota: los años bisiestos son cada cuatro años,
pero no cada 100, aunque sí cada 400. Por
ejemplo, el año 2000, es múltiplo de 4, por
lo tanto debería haber sido bisiesto, pero
también es múltiplo de 100, por lo tanto no
debería serlo; aunque, como también es
múltiplo de 400, finalmente lo fue.
Seguimos con el tema de las fechas. Ahora 
escribir dos funciones más. La primera debe
responder a este prototipo: int Comparar
Fechas(fecha, fecha);
. Debe comparar las
dos fechas suministradas y devolver 1 si la
primera es mayor, -1 si la segunda es mayor
y 0 si son iguales.
La otra función responderá a este prototipo: int Diferencia(fecha, fecha);, y debe devolver la diferencia en días entre las dos fechas suministradas.
 
  Hoy habia 2 visitantes (2 clics a subpáginas) ¡Aqui en esta página!  
 
Este sitio web fue creado de forma gratuita con PaginaWebGratis.es. ¿Quieres también tu sitio web propio?
Registrarse gratis