Agrupación de datos mediante structs en C

Cuando necesitas representar una entidad que tiene múltiples propiedades relacionadas —como un punto en un plano con coordenadas x e y, o un usuario con nombre y edad— las variables sueltas se vuelven inmanejables. Para esto usamos las estructuras (struct), que son tipos de datos compuestos que permiten agrupar diferentes variables bajo un mismo nombre.

A nivel interno, un struct es simplemente un bloque de memoria contiguo donde el compilador reserva espacio para cada uno de sus miembros, asegurándose de que cada uno empiece en la dirección de memoria correcta. En C, la definición de un struct crea un “tag” (una etiqueta), que es el nombre técnico de ese tipo. Por ejemplo, si defines struct Punto { int x; int y; };, para declarar una variable no puedes escribir simplemente Punto p;, sino que debes usar la palabra clave struct: struct Punto p;. Esto ocurre porque, en el estándar de C, el “tag” reside en un espacio de nombres distinto al de las variables comunes. Para evitar esta verbosidad, usamos typedef, que nos permite crear un alias y usar el nombre directamente, como si fuera un tipo nativo.

Para usar esto correctamente, debes saber cuándo aplicarlo: usa struct siempre que la semántica de tu programa requiera agrupar datos que no pueden separarse sin perder el sentido. Si lo haces mal —por ejemplo, intentando acceder a un miembro de un puntero usando el punto . en lugar de la flecha ->, o intentando comparar dos estructuras con el operador ==— el compilador te lanzará un error de sintaxis o de tipo, ya que C no tiene una lógica predefinida para comparar bloques de memoria complejos.

#include <stdio.h>

/* Definición con tag: requiere usar 'struct Punto' para declarar variables */
struct PuntoRaw {
    int x;
    int y;
};

/* Definición con typedef: permite usar 'Punto' directamente (más limpio) */
typedef struct {
    int x;
    int y;
} Punto;

/* Función que recibe la estructura por valor (se crea una copia en la pila) */
void imprimir_punto(Punto p) {
    printf("Coordenadas: x = %d, y = %d\n", p.x, p.y);
}

/* Función que recibe un puntero para modificar la estructura original */
void desplazar(Punto *p, int dx, int dy) {
    p->x += dx; // El operador -> es azúcar sintáctico para (*p).x
    p->y += dy;
}

int main(void) {
    // 1. Inicialización posicional (el orden importa)
    Punto p1 = {10, 20};

    // 2. Inicializadores designados [C99]: más seguro y legible
    // Los campos no mencionados se inicializan automáticamente a cero
    Punto p2 = {.x = 5, .y = 15};

    // 3. Copia por asignación: se copian todos los valores de los miembros
    Punto p3 = p1;

    // 4. Uso de punteros
    Punto *ptr = &p2;

    printf("P1 (inicial): ");
    imprimir_punto(p1);

    printf("P2 (inicial): ");
    imprimir_punto(p2);

    printf("P3 (copia de P1): ");
    imprimir_punto(p3);

    printf("P2 (después de desplazar): ");
    desplazar(&p2, 5, 5);
    imprimir_punto(p2);

    printf("P2 (a través de puntero): ");
    ptr->x = 100;
    imprimir_punto(*ptr);

    return 0;
}

Análisis del código

En el ejemplo anterior, empezamos con PuntoRaw. Si intentáramos declarar PuntoRaw p; en el main, el compilador fallaría; tendríamos que escribir struct PuntoRaw p;. Para evitar esto, aplicamos la técnica de typedef con una estructura anónima, permitiéndonos usar simplemente Punto como un tipo de dato limpio.

Al inicializar p1, usamos la inicialización posicional: el primer valor asignado va a x y el segundo a y. Sin embargo, con p2, utilizamos inicializadores designados {.x = 5, .y = 15} [C99]. Esta es la práctica recomendada en sistemas críticos porque, si el orden de los miembros en la definición del struct cambia, tu código seguirá funcionando correctamente y es mucho más evidente qué valor pertenece a qué campo.

Fíjate en p3 = p1. En C, las estructuras se pueden copiar mediante una asignación simple. El compilador realiza una copia de bits de la memoria de p1 hacia p3. Esto es una copia por valor; cualquier cambio posterior en p1 no afectará a p3.

Para la manipulación eficiente de datos, usamos punteros. En la función desplazar, recibimos Punto *p. Para acceder a x o y a través de ese puntero, usamos el operador flecha ->. Es fundamental entender que p->x es simplemente una forma abreviada de escribir (*p).x. El paréntesis es obligatorio si usas el punto, porque el operador de acceso al miembro . tiene una precedencia mayor que el operador de desreferencia *.

El error frecuente

Un error muy común es intentar comparar dos estructuras directamente usando el operador de igualdad:

if (p1 == p2) { // ERROR DE COMPILACIÓN
    // ...
}

C no permite esto porque no sabe si quieres comparar la memoria exacta (incluyendo el padding) o solo los valores de los miembros. Si necesitas comparar, debes hacerlo campo a campo (if (p1.x == p2.x && p1.y == p2.y)) o usar memcmp de <string.h>.

Sin embargo, usar memcmp es peligroso. Las estructuras suelen tener padding (espacio de relleno) que el compilador inserta para alinear los miembros en la memoria según la arquitectura (por ejemplo, para que un int empiece en un múltiplo de 4). memcmp compara cada byte, incluyendo ese relleno, que contiene basura de la pila. Dos estructuras con los mismos valores lógicos podrían resultar “desiguales” en memcmp debido a que el contenido de su padding es distinto.

60

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio