Tipos de objetos III: Estructuras
Las estructuras son el segundo tipo de datos estructurados que veremos (valga la redundancia).
Al contrario que los arrays, las estructuras nos permiten agrupar varios datos, que mantengan algún tipo de relación, aunque sean de distinto tipo, permitiendo manipularlos todos juntos, usando un mismo identificador, o cada uno por separado.
Las estructuras son llamadas también muy a menudo registros, o en inglés records. Tienen muchos aspectos en común con los registros usados en bases de datos. Y siguiendo la misma analogía, cada objeto de una estructura se denomina a menudo campo, o field.
Sintaxis:
struct [<identificador>] {
[<tipo> <nombre_objeto>[,<nombre_objeto>,...]];
} [<objeto_estructura>[,<objeto_estructura>,...]; |
El identificador de la estructura es un nombre opcional para referirse a la estructura.
Los objetos de estructura son objetos declarados del tipo de la estructura, y su inclusión también es opcional. Sin bien, aún siendo ambos opcionales, al menos uno de estos elementos debe existir.
En el interior de una estructura, entre las llaves, se pueden definir todos los elementos que consideremos necesarios, del mismo modo que se declaran los objetos.
Las estructuras pueden referenciarse completas, usando su nombre, como hacemos con los objetos que ya conocemos, y también se puede acceder a los elementos definidos en el interior de la estructura, usando el operador de selección (.), un punto.
Una vez definida una estructura, es decir, si hemos especificado un nombre para ella, se puede usar igual que cualquier otro tipo de C++. Esto significa que se pueden declarar más objetos del tipo de estructura en cualquier parte del programa. Para ello usaremos la forma normal de declaración de objetos, es decir:
[struct] <identificador> <objeto_estructura>
[,<objeto_estructura>...]; |
En C++ la palabra struct es opcional en la declaración de objetos, al contrario de lo que sucede en C, en el que es obligatorio usarla.
Ejemplo:
struct Persona {
char Nombre[65];
char Direccion[65];
int AnyoNacimiento;
} Fulanito; |
Este ejemplo define la estructura Persona y declara a Fulanito como un objeto de ese tipo. Para acceder al nombre de Fulanito, por ejemplo para visualizarlo, usaremos la forma:
cout << Fulanito.Nombre; |
Funciones en el interior de estructuras
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, y su uso está muy relacionado con la programación orientada a objetos.
Aunque esta característica se usa casi exclusivamente con las clases, como veremos más adelante, también puede usarse en las estructuras. De hecho, en C++, las diferencias entre estructuras y clases son muy tenues.
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 símbolo "~".
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] y un espacio (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, ya que si necesitamos usar constructores para asignar valores iniciales, será mucho más lógico usar clases que estructuras.
Mencionar aquí, sólo a título de información, que el constructor no tiene por qué ser único. Se pueden definir 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.
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;
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 e incluso, redefinir operadores. Y en general, las estructuras iten 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 objetos del mismo tipo de estructura, (salvo que se usen constructores), y funciona como la intuición nos 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;.
Quizás te hayas quedado intrigado por el comentario anterior, que adelantaba que se pueden asignar estructuras diferentes, siempre que se usen los constructores adecuados.
Esto, en realidad, se puede extender a cualquier tipo, no sólo a estructuras. Por ejemplo, definiendo el constructor adecuado, podemos asignar un entero a una estructura. Veamos cómo hacer esto.
Hasta ahora, los constructores que hemos visto no usaban argumentos, pero eso no significa que no puedan tenerlos.
Crearemos como ejemplo, una estructura para manejar números complejos. Un número complejo está compuesto por dos valores reales, el primero contiene lo que se llama la parte real y el segundo la parte imaginaria.
struct complejo {
double real;
double imaginario;
}; |
Esta estructura es suficiente para muchas de las cosas que podemos hacer con números imaginarios, pero aprovechando que podemos crear funciones, podemos añadir algunas que hagan de una forma más directa cosas que de otro modo requieren añadir código externo.
Por ahora nos limitaremos a añadir unos cuantos constructores. El primero es el más lógico: un constructor por defecto:
struct complejo {
complejo() { real=0; imaginario = 0; }
double real;
double imaginario;
}; |
Este construtor se usará, por ejemplo, si declaramos un array:
complejo array[10]; |
El constructor por defecto será llamado para cada elemento del array, aunque no aparezca tal llamada en ningún punto del programa.
Otro constructor nos puede servir para asignar un valor a partir de dos números:
struct complejo {
complejo() { real=0; imaginario = 0; }
complejo(double r, double i) { real=r; imaginario = i; }
double real;
double imaginario;
}; |
Mediante este constructor podemos asignar valores inciales en la declaración:
complejo c1(10.23, 213.22); |
Los números reales se consideran un subconjunto de los imaginarios, en los que la parte imaginaria vale cero. Esto nos permite crear otro constructor que sólo ita un valor real:
struct complejo {
complejo() { real=0; imaginario = 0; }
complejo(double r, double i) { real=r; imaginario = i; }
complejo(double r) { real=r; imaginario = 0; }
double real;
double imaginario;
}; |
Este constructor nos permite, como en el caso anterior, inicializar un valor de un complejo en la declaración, pero también nos permite asignar un valor double a un complejo, y por el sistema de promoción automático, también podemos asignar valores enteros o en coma flotante:
complejo c1(19.232);
complejo c2 = 1299.212;
int x = 10;
complejo c3 = x; |
Este tipo de constructores se comportan como conversores de tipo, nada nos impide crear constructores con cualquier tipo de parámetro, y tales constructores se podrán usar para convertir cualquier tipo al de nuestra estructura.
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 tienen un ámbito local, y para acceder a ellas se necesita hacer referencia a la estructura más externa.
Por ejemplo para declarar un objeto del tipo stNombre hay que utilizar el operador de (::):
stPersona::stNombre NombreAuxiliar; |
Sin embargo para declarar un objeto de tipo stDireccion basta con declararla:
stDireccion DireccionAuxiliar; |
Comentarios