Estructuras:
Las estructuras son conjuntos de variables relacionadas que se manejan de forma conjunta. Esas variables relacionadas se llaman “miembros” y pueden ser de tipos distintos (no como en un array). Un ejemplo típico sería:

Como se puede ver, los tipos de los miembros pueden ser simples (ej. int, double, float, etc.) o complejos (ej. arrays, punteros, etc.). Es más, podrían ser incluso otras estructuras.
Una cuestión importante, y que suele ser fuente de confusión, es que “struct cliente” es en realidad el tipo de datos. Luego se pueden definir variables de este tipo, por ejemplo, con:
struct cliente mi_cliente;
Con la variable mi_cliente podemos referirnos a todo el conjunto de variables de la estructura, lo cual es práctico. También es posible acceder a miembros concretos con el operador “.”, es decir:
edad = mi_cliente.edad;
Con frecuencia, las estructuras se manejan por referencia, es decir, mediante un puntero. Por ejemplo, pcliente sería un puntero a una estructura de tipo struct cliente:
struct cliente *pcliente;
Este uso conjunto de punteros y estructuras es tan habitual que, incluso, C define una sintaxis específica para el acceso a los miembros de las estructuras apuntadas por punteros. Así, (*pcliente).nombre, que es el miembro nombre de la estructura apuntada por el puntero pcliente, también se puede escribir:
pcliente->nombre;
Las variables de tipo estructura se pueden inicializar, asignar, pasar a funciones (por valor y por referencia), retornar de funciones, etc. También es posible definir arrays de estructuras y, por supuesto, estructuras cuyos miembros son arrays o cadenas.
sizeof y sizeof():
El operador sizeof permite conocer el tamaño de cualquier variable y, en particular, de una estructura.
En general, el tamaño de una estructura será la suma de los tamaños de sus miembros, pero no siempre tiene que ser así. Hay procesadores o compiladores que exigen que las palabras (palabra = 2 bytes) empiecen en posición par o impar de la memoria, lo que significa que, si una estructura incluye miembros que ocupan un byte (ej. char) y miembros que ocupan palabras (ej. int), estos últimos tendrán que ser “alineados” para empezar en la posición de memoria correcta, utilizándose para ello bytes de relleno. Por ello, lo mejor es no confiar en la suma de tamaños y usar siempre el operador size.
Por otro lado, sizeof() es similar a size, pero opera sobre tipos, no variables.
Uniones:
Las uniones son parecidas a las estructuras, en el sentido de que son una forma de agrupar un conjunto de variables relacionadas (los miembros):

La principal diferencia es que, mientras que en una estructura todos los miembros tienen sentido y existencia en todo momento, en el caso de una unión, sólo uno de los miembros existe en cada momento.
Por ejemplo, en el caso anterior, la fecha indicada estará almacenada en formato ddmmaa o en formato aammdd, pero no en ambos. Y en ambos casos se reutilizan las mismas posiciones de memoria. De hecho, es el programador el que tiene que saber qué dato está almacenado y actuar en consecuencia, porque si no lo controla puede recuperar información sin sentido.
Otro buen ejemplo lo tenemos en el header file _vic2.h de cc65. En este header file se definen los tipos de datos que dan acceso a los registros del chip de vídeo del C64 (VIC o, mejor dicho, VIC-II). Por ejemplo, para acceder y/o modificar las posiciones de los sprites se usan las posiciones:
SP0X, SP0Y | $d000 y $d001 |
SP1X, SP1Y | $d002 y $d003 |
SP2X, SP2Y | $d004 y $d005 |
SP3X, SP3Y | $d006 y $d007 |
SP4X, SP4Y | $d008 y $d009 |
SP5X, SP5Y | $d00a y $d00b |
SP6X, SP6Y | $d00c y $d00d |
SP7X, SP7Y | $d00e y $d00f |
Hay dos formas de ver o entender estos 16 bytes:
- Como 16 posiciones independientes, cada una con su nombre y dirección.
- O como un array de 8 parejas de posiciones (x, y).
Y esto, precisamente, es lo que hace _vic2.h mediante una unión:
- Como 16 posiciones independientes, cada una con su nombre y dirección:

- O como un array de 8 parejas de posiciones (x, y):

Es decir, el primer miembro de la unión es la estructura de 16 posiciones independientes, y el segundo miembro de la unión es el array de 8 parejas (x, y).
Se puede acceder a las posiciones de los sprites de la primera forma, mediante sus nombres (spr0_x, spr0_y, etc.), o de la segunda forma, mediante el array (spr_pos[i].x, spr_pos[i].x, etc.), pero en ambos casos nos estamos refiriendo a las mismas posiciones de memoria.
En definitiva, las uniones son formas alternativas de utilizar unas mismas posiciones de memoria. Por lo demás, las uniones tienen usos muy parecidos a los de las estructuras, con “.” y “->” para acceder a los miembros, permitiéndose inicialización, asignación, paso por valor y referencia a funciones, etc.
typedef:
En C hay tipos de datos simples (char, int, float y double) y tipos de datos compuestos o estructurados (estructuras y uniones). En ambos casos es posible usar typedef para definir sinónimos para los tipos de datos.
Por ejemplo, con estos usos de typedef:

conseguimos que “byte” sea equivalente a “unsigned char” y que “Fecha_ddmmaa” sea equivalente a “struct ddmmaa”.
Esto es especialmente útil en el caso de estructuras y uniones porque, como se ha visto en los apartados anteriores, al definir estructuras y uniones en realidad estamos definiendo tipos, y los nombres de esos tipos son “struct ddmmaa” y “union fecha”. Con typedef es posible tener nombres de tipos más compactos.
Código de ejemplo: estructuras