Estructuras#
Típicamente, en todo programa no minúsculo, se trabaja con datos que están relacionados entre sí.
Por ejemplo, con puntos en \(\mathbb{R}^n\), con figuras geométricas, con usuarios, con sensores, etc.
Hasta ahora, la solución para manipular datos en \(\mathbb{R}^2\) sería tener 2 variables que nos sirvan para identificar
\(x\) e \(y\) (sean 2 float
o 2 arreglos), para manipular usuarios tendríamos que tener todos los datos importantes
(un identificador, un nombre, un nombre de usuario, y otros que dependerán de la aplicación).
Por ejemplo, pensemos una aplicación que opera con vectores (que inician en el origen): utilizaremos 3 float
para
almacenar las coordenadas del vector, e implementaremos funciones que suman, restan, multiplican vectores por
constantes, etc.
Ya con la función que suma dos vectores vemos que:
- la función tiene demasiados parámetros para lo poco que hace,
- cada función que trabaje con vectores deberá tener igual cantidad de parámetros.
Sin embargo, veamos cómo se invoca a esta función, y el código relevante para ello:
Claramente, no es algo cómo para manipular, y por eso tenemos estructuras.
¿Qué son las estructuras?#
Las estructuras son la herramienta que nos da el lenguaje de programación C para crear tipos compuestos de datos. De este modo, podemos agregar variables que estén relacionadas en un nuevo tipo de dato, encapsulando la información. Esto no sólo nos permite manipular las variables en bloque, sino que nos permite asignarle un nombre al conjunto para hacer referencia a éste de una forma más sencilla, legible y coherente.
La sintaxis para definir una estructura es:
Definición de estructuras | |
---|---|
Una vez que tenemos la estructura definida, podemos declarar una variable según la sintaxis:
Declaración de variables usando estructuras | |
---|---|
Retomando el ejemplo de los vectores, para definir una estructura con las 3 componentes del vector escribiríamos:
obteniendo así un nuevo tipo llamado struct vector3
.
Al igual que con cualquier variable, para declarar una variable del tipo struct vector3
escribimos:
Ejemplo: declaración de una variable de tipo `struct vector` | |
---|---|
donde vec1
es el nombre de la variable.
Reescribamos el prototipo de la función que suma vectores utilizando este nuevo conocimiento:
Ejemplo: función que suma vectores (prototipo) | |
---|---|
En este ejemplo vemos, en la definición del puntero, la misma sintaxis que utilizamos para todos los demás punteros que
hemos visto (por ejemplo, int *
).
Por lo tanto, podemos crear una variable de tipo struct vector3
y tener un puntero a dicha variable con struct
vector3 *
.
Redefinición del nombre#
Siempre que utilizamos estructuras trabajamos con nombres que comienzan con struct
.
Si bien esto es claro y nos recuerda que el dato que utilizamos es una estructura, lo típico es definir un tipo nuevo a
partir de la estructura utilizando typedef
.
Tal y como vimos en otros casos, la definición del nuevo tipo lleva la sintaxis:
por lo que podemos definir la estructura como
Además, cuando se redefine el nombre como en estos casos, es posible definir la estructura y el nuevo nombre en la misma sentencia, siguiendo la misma convención que antes:
Esto es similar a haber hecho
y el prototipo de la función se vuelve más claro también:
Ejemplo: función que suma vectores (prototipo) | |
---|---|
Convenciones
En este caso, utilicé una convensión típica en la que se define un tipo nuevo con sufijo _t
.
Otra convensión utiliza camelCase, definiendo el tipo como Vector3
en lugar de vector3_t
, es decir:
En estos casos, suele utilizarse el camelCase incluso al definir la estructura, con la siguiente notación:
o bien
con lo cual, el prototipo antes visto es
¿Cuánto espacio, cuánta memoria, ocupa una estructura?#
Para comenzar, como tenemos un nuevo tipo, podemos saber en tiempo de compilación cuánto ocupa dicho tipo, la
estructura, utilizando sizeof
: sizeof(struct <nombre>)
.
En el caso del ejemplo del vector, tenemos: sizeof(struct vector3)
o bien sizeof(vector3_t)
.
Dado que es una composición de elementos, una estructura ocupa al menos lo mismo que la suma del almacenamiento de sus elementos pero, por cuestiones de alineamiento de los datos, puede ocupar más, es decir:
para el caso del vector.
Ahora, volvamos sobre el prototipo de la función vector_suma()
:
Tanto los parámetros v1
como v2
se pasan por copia, es decir, toda la información de la estructura debe ser copiada
para pasársela a la función.
Esto es muy ineficiente.
Por eso, las estructuras siempre se pasan por puntero, y no únicamente cuando se desea modificarlas.
En este caso, si no se desea modificar la estructura, se pasa a través de un puntero a una estructura constante.
Entonces, el prototipo nos queda:
que, por otro lado, es más simple de leer haciendo uso de la definición del tipo vector3_t
:
Inicialización#
Al momento de definir una variable de un tipo que es una estructura se pueden asignar valores a cada uno de sus
componentes.
Para ello hay 2 formas de hacerlo: en orden o por nombres.
En un primer modo, en orden, la estructura se inicializa como si fuese un arreglo, dándole valor a cada uno de los
miembros de la estructura en el orden en que fue definida.
En el caso de la estructura struct vector3
haríamos:
mientras que si cargamos los miembros indicandos sus nombres haríamos:
struct vector3 vec1 = {.x = 0.2, .y = 19.3, .z = 1e-2};
Típicamente, incluso con los nombres se inicializan en el orden en que fueron declarados los miembros. La elección entre un modo u otro es meramente por claridad a la hora de leer el código.
Acceso#
Hemos visto cómo definir estructuras, pero no cómo acceder a sus miembros.
El operador que debemos utilizar es el .
, un punto.
De este modo, si definimos las variables
podemos acceder a sus elementos del siguiente modo:
v1.x = 3.5; /* escritura del elemento x de v1 */
v1.y = v2.x; /* lectura del elemento x de v2 y escritura de y en v1 */
v1.z = v2.z; /* lectura de z en v2 y escritura de z en v1 */
Por otro lado, si en lugar de la estructura tenemos un puntero a la misma, se cumplen las mismas reglas que con todos
los punteros: podemos utilizar el operador de indirección (*
):
vector3_t v1;
struct vector3 v2;
struct vector3 *pv1;
vector3_t *pv2;
pv1 = &v1;
pv2 = &v2;
(*pv1).x = 3.5;
(*pv1).y = (*v2).x;
(*pv1).z = (*v2).z;
Sin embargo, esta escritura se vuelve engorrosa cuando tenemos múltiples estructuras anidadas.
Supongamos una estructura empleado_t
.
Esta estructura está compuesta por dos cadenas de caracteres dinámicas (char *
) para el nombre y apellido, un
puntero a una estructura de tipo empresa_t
que define la empresa en la que trabaja el empleado, un enumerativo para su
cargo y un número para su salario.
A su vez, la estructura empresa_t
está definida por cadenas de caracteres dinámicas para su razón social, su CUIT y su
dirección.
La figura 1 nos da un esquema de este tipo.
empleado_t
con estructuras anidadas.El código que implementa esta la figura 1 se muestra a continuación:
Luego, podemos definir variables y utilizarlas del siguiente modo (utilizando notación de punteros):
Como podemos ver, es una notación sumamente ofuscada.
Por eso se utiliza el operador ->
para acceder a los miembros de una estructura cuando se posee un puntero a la misma.
El mismo ejemplo que antes, pero utilizando el operador ->
se convierte en:
Vemos que la notación es <puntero a estructura> -> <miembro>
.
Como aclaración, dependiendo de la fuente que utilice el navegador podríamos ver el operador como una flecha, pero en
realidad es un guión -
y un >
juntos.
Ejemplo: creación de estructuras
¿Cómo haríamos si quisiéramos crear una estructura del tipo empresa_t
?
¿Y una estructura del tipo empleado_t
?
A continuación se dan ejemplos de funciones que cargar/crean estructuras de tipo empresa_t
Notar que esta función no crea una estructura, sino que carga los datos en una ya creada. Por ejemplo:
Ejercicio
La implementación de las funciones análogas para empleado_t
se dejan al lector.
Un ejemplo de una función que recibe una estructura, constante, es aquella que la imprime, por ejemplo:
Arreglos de estructuras#
Los arreglos de estructuras podemos definirlos de 2 maneras, como todo arreglo: estático o dinámico.
Un arreglo estático se define como cualquier otro arreglo, considerando que el tipo ahora es struct nombre
o
nombre_t
:
Esta sentencia crea un arreglo de 100 estructuras de tipo vector3_t
y ocupa un espacio dado por sizeof(vectores)
, que es igual a 100 * sizeof(vector3_t)
.
Si lo definimos en forma dinámica, tenemos que recordar que necesitamos un puntero a una estructura (o muchas, que es lo mismo para el puntero) y su cantidad. En este caso, para crear un arreglo de 100 estructuras agregaremos las sentencias:
vector3_t *vectores;
...
vectores = (vector3_t *) malloc(100 * sizeof(vector3_t));
if (NULL == vectores) { ... }
o bien, lo podemos hacer con calloc
:
vector3_t *vectores;
...
vectores = (vector3_t *) calloc(100, sizeof(vector3_t));
if (NULL == vectores) { ... }
Luego, podemos acceder a cualquier estructura del arreglo mediante el índice, y accedemos a los miembros de la
estructura utilizando el operador .
como se muestra a continuación:
Arreglo de punteros a estructuras#
En este caso, lo más común es tener arreglos dinámicos de estructuras también dinámicas.
Para ello, tenemos que definir un puntero doble, es decir, un arreglo (dinámico) de punteros a estructuras.
Esto lo hacemos con la sentencia que define punteros dobles, para struct nombre
o nombre_t
.
Sea la estructura estudiante_t
definida como
podemos definir el arreglo de punteros como
Luego, podemos asignarle un bloque de memoria dinámica utilizando malloc()
:
Finalmente, dada una función que crea un estudiante
en forma dinámica (que podría seguir el prototipo estudiante_t * estudiante_crear(const char *nombre, const char *apellido)
) utilizaríamos:
Las líneas resaltadas (14 y 29) son las que crean el arreglo y crean los estudiantes en forma dinámica.
Notar que se validan todos los ingresos de datos.
Modularización#
Generalmente, cuando se trabaja con estructuras, estamos implementando funciones que manipulan las estructuras, que las
crean, o las destruyen, tanto para hacer legible el código como para evitar repetirlo y darle coherencia.
Imaginen el código escrito en la sección anterior donde, en lugar de utilizar las funciones
destruir_arreglo_estudiantes
y estudiante_crear
en la función crear_arreglo_interactivamente
, se agrega el código
de las primeras directamente en el cuerpo de la última (¡3 veces repetiríamos el código de
destruir_arreglo_estudiantes
!).
Por ello, una modularización clásica suele contener al menos un archivo .h
y un archivo .c
.
En el encabezado se define la estructura y los prototipos, mientras que en el archivo de implementación se
implementan—valga la redundancia—las función definidas en el encabezado.
Modularización: estudiantes
Biblioteca estándar para manejo de estructuras#
Dado que las estructuras son intrínsecamente un tipo de dato creado por el desarrollador, no hay bibliotecas para manejar estructuras arbitrarias.
Guías de ejercicios#
La guía de ejercicios de estructuras se encuentra aquí.