La composición de tipos complejos mediante la agrupación de otros tipos más simples es la base para modelar datos jerárquicos en C. Al anidar un struct dentro de otro, creas una estructura de datos donde el acceso a los miembros se realiza de forma encadenada, por ejemplo, r.origen.x. Esta técnica permite organizar la memoria de forma lógica y contigua, mapeando los campos de la estructura anidada directamente en offsets respecto a la dirección base de la estructura principal.
Sin embargo, esta composición tiene reglas estrictas de diseño. Si intentas definir una estructura que se contiene a sí misma de forma directa (por ejemplo, struct Nodo { struct Nodo siguiente; };), el compilador lanzará un error porque el tamaño de la estructura sería infinito. La solución para implementar estructuras de datos dinámicas, como listas o árboles, es utilizar punteros [C89/C90]. Un puntero a una estructura es un tipo de tamaño fijo (normalmente 4 u 8 bytes) que permite la declaración adelantada (forward declaration): el compilador solo necesita saber que es un puntero para permitir el uso de la estructura, sin conocer su tamaño completo aún.
Cuando pases estructuras a funciones, debes decidir entre pasar por valor o por puntero. Pasar por valor realiza una copia completa de todos los datos de la estructura en el stack; esto es costoso si la estructura es grande, pero seguro si no quieres que la función modifique el original. Pasar por puntero es extremadamente eficiente ya que solo trasladas una dirección de memoria, permitiendo además que la función altere el contenido original. Las estructuras pequeñas, de unos pocos campos simples, se pueden devolver por valor de forma eficiente, ya que el compilador suele optimizar esto mediante registros o mediante Named Return Value Optimization (NRVO) [C99].
Finalmente, hay un detalle físico que el código suele ignorar: el padding (relleno). Para asegurar que la CPU acceda a los datos de forma alineada y eficiente, el compilador inserta bytes invisibles entre los miembros de un struct. Esto significa que dos estructuras con los mismos valores lógicos podrían ser distintas si sus bytes de relleno contienen basura de la memoria, un detalle que vuelve errático el uso de memcmp.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
// Estructura básica para composición
typedef struct {
int x;
int y;
} Punto;
// Estructura anidada: Rectangulo contiene dos instancias de Punto
typedef struct {
Punto origen;
Punto esquina;
} Rectangulo;
// Estructura autorreferencial para listas enlazadas
typedef struct Nodo {
int dato;
struct Nodo *siguiente;
} Nodo;
// Devuelve una estructura por valor (eficiente para tipos pequeños/medianos)
Rectangulo crear_rectangulo(int x, int y, int ancho, int alto) {
Rectangulo r = {
.origen = {x, y},
.esquina = {x + ancho, y + alto}
};
return r;
}
// Modifica la estructura original mediante un puntero
void desplazar(Rectangulo *r, int dx, int dy) {
if (!r) return;
r->origen.x += dx;
r->origen.y += dy;
r->esquina.x += dx;
r->esquina.y += dy;
}
// Estructura para demostrar el problema del padding
typedef struct {
char letra;
// El compilador insertará 3 bytes de padding aquí para alinear el int
int valor;
} DatosConPadding;
int main(void) {
// 1. Uso de anidamiento y acceso encadenado
Rectangulo rect = crear_rectangulo(10, 10, 50, 30);
printf("Origen: (%d, %d)\n", rect.origen.x, rect.origen.y);
// 2. Uso de punteros para modificar valores
desplazar(&rect, 5, 5);
printf("Nuevo origen tras desplazar: (%d, %d)\n", rect.origen.x, rect.origen.y);
// 3. Estructuras autorreferenciales (lista simple)
Nodo *n1 = malloc(sizeof(Nodo));
Nodo *n2 = malloc(sizeof(Nodo));
if (!n1 || !n2) {
perror("Fallo en malloc");
return EXIT_FAILURE;
}
n1->dato = 1;
n1->siguiente = n2;
n2->dato = 2;
n2->siguiente = NULL;
printf("Nodo 1: %d -> Nodo 2: %d\n", n1->dato, n1->siguiente->dato);
// 4. Demostración de la comparación de memoria y el padding
DatosConPadding d1, d2;
// Inicialización manual (el padding queda con valores indeterminados)
d1.letra = 'A';
d1.valor = 100;
// Inicialización mediante un bloque de ceros para limpiar padding
memset(&d2, 0, sizeof(DatosConPadding));
d2.letra = 'A';
d2.valor = 100;
// memcmp puede fallar aunque d1 y d2 sean "iguales" lógicamente
// debido a que el padding de d1 contiene basura de la pila.
if (memcmp(&d1, &d2, sizeof(DatosConPadding)) == 0) {
printf("Las estructuras son idénticas en memoria.\n");
} else {
printf("Las estructuras difieren en memoria (causa probable: padding).\n");
}
// Limpieza
free(n1);
free(n2);
return EXIT_SUCCESS;
}
Análisis del código
En el ejemplo, la función crear_rectangulo utiliza la sintificación de diseño para inicializar la estructura Rectangulo de forma legible. Fíjate cómo accedemos a los miembros de origen mediante puntos consecutivos: rect.origen.x. Internamente, el compilador calcula la dirección de x sumando el offset de origen y luego el offset de x sobre la base de rect.
En desplazar, utilizamos el operador ->, que es la forma abreviada de desreferenciar un puntero y acceder a un miembro ((*r).origen.x). Esto es fundamental para la eficiencia: en lugar de copiar los 16 bytes (asumiendo 4 bytes por int) que ocupa un Rectangulo, solo pasamos 8 bytes (en sistemas de 64 bits) que representan la dirección de memoria.
La estructura Nodo demuestra la resolución de la recursividad. El campo struct Nodo *siguiente no contiene la estructura completa, sino una dirección. Esto permite que Nodo sea una estructura de tamaño finito y que los nodos se enlacen dinámicamente en el heap.
Finalmente, el experimento con DatosConPadding revela un peligro crítico. Aunque d1.letra, d1.valor, d2.letra y d2.valor son idénticos, memcmp compara la totalidad de los bytes de la región de memoria. Si no has limpiado la memoria con memset antes de usar una estructura, los bytes de relleno (padding) entre letra y valor contendrán basura, haciendo que memcmp devuelva un valor distinto de cero.
El error frecuente
Un error clásico es confiar en memcmp para comparar la igualdad de dos estructuras que contienen tipos de datos con diferentes alineaciones o que han sido parcialmente inicializadas.
struct MalEjemplo {
char c;
// 3 bytes de padding aquí
int i;
};
struct MalEjemplo m1 = {'a', 1};
struct MalEjemplo m2;
m2.c = 'a';
m2.i = 1;
// ¡ERROR! m1.padding contiene basura de la pila.
// m2.padding puede ser ceros.
// memcmp fallará aunque lógicamente sean iguales.
if (memcmp(&m1, &m2, sizeof(struct MalEjemplo)) == 0) {
// Este bloque es impredecible
}
Para evitarlo, utiliza siempre memset para asegurar que la memoria esté limpia antes de inicializar la estructura, o compara los miembros uno a uno si el rendimiento de la comparación campo a campo es aceptable para tu caso de uso.
N° 62