Union in Linguaggio C

In linguaggio C esiste un tipo di dati strutturato chiamato union. Le union sono simili alle strutture dati, ma hanno alcune caratteristiche peculiari che le rendono particolarmente utili in alcuni casi.

In particolare una union possiede dei membri come una struttura dati, ma in ogni istante di tempo solo uno di essi può contenere un valore valido. Questo perché i membri di una union condividono lo stesso spazio di memoria.

In questa lezione vedremo le basi per dichiarare, inizializzare ed accedere ad una union in linguaggio C.

Union

Una union è un particolare costrutto del linguaggio C molto simile ad una struttura dati struct. Come una struct, una union consiste di uno o più membri, anche di tipo differente.

Tuttavia, a differenza di una struttura dati, il compilatore alloca esclusivamente lo spazio necessario per contenere il membro di dimensioni più grandi. Questo significa che tutti i membri di una union condividono lo stesso spazio di memoria.

Pertanto, quando si assegna un valore ad un membro di una union viene sovrascritto il valore di tutti gli altri membri.

Per comprendere meglio prendiamo un esempio. Supponiamo di dichiarare la seguente union composta da tre membri:

union {
    int i;
    double d;
    char c;
} u;

La dichiarazione di questa union è molto simile alla dichiarazione di una struttura. Ad esempio:

struct {
    int i;
    double d;
    char c;
} s;

La differenza principale tra la struct s e la union u è che i membri di s sono memorizzati in spazi di memoria separati (ossia indirizzi differenti) mentre i membri di u sono memorizzati nello stesso spazio di memoria (ossia nello stesso indirizzo).

Assumendo che un intero occupi 4 byte, la disposizione in memoria di s è la seguente:

Disposizione in memoria della struttura
Figura 1: Disposizione in memoria della struttura

Mentre la disposizione in memoria di u è la seguente:

Disposizione in memoria della union
Figura 2: Disposizione in memoria della union

Nella struttura s, i membri i, d e c occupano locazioni differenti, per cui la dimensione totale di s è di 13 byte (8 per il double, 4 per l'int e 1 per il char).

Viceversa, nella union u, i membri i, d e c occupano lo stesso spazio di memoria, per cui la dimensione totale di u è di 8 byte (in quanto la dimensione massima è quella del membro double che occupa 8 byte).

Definizione

Union

In linguaggio C una union è un tipo di dati in grado di memorizzare in maniera esclusiva elementi di tipi differenti.

La sintassi per dichiarare una union è la seguente:

union nome_union {
    tipo1 nome1;
    tipo2 nome2;
    ...
    tipoN nomeN;
};

In un qualunque istante il contenuto della union è rappresentato dall'ultimo membro assegnato.

Accesso ai membri di una union

Per accedere ai membri di una union si procede nello stesso modo in cui si accede ai membri di una struttura: con l'operatore punto ..

Per cui, definendo una union in questo modo:

union {
    int i;
    double d;
    char c;
} u;

Si può accedere ai membri i, d e c con le seguenti istruzioni:

u.i = 10;
u.d = 3.14;
u.c = 'a';

Tuttavia, dato che i membri della union occupano lo stesso spazio di memoria, quando si assegna un valore ad un membro, viene sovrascritto il valore di tutti gli altri membri. Per cui, se assegniamo un valore ad u.i, il valore memorizzato in u.d viene perduto.

All'atto pratico, in realtà, vengono sovrascritti i 4 byte inferiori del membro d, ossia del double. Per cui, se provassimo ad accedere al membro d magari quest'ultimo non ha significato.

Chiariamo con un esempio:

#include <stdio.h>

int main() {
    union {
        int i;
        double d;
        char c;
    } u;

    /* Assegniamo dapprima il valore intero */
    u.i = 10000;

    /* Stampiamo il valore intero */
    printf("Valore intero: %d\n", u.i);

    /* Assegniamo il valore double */
    u.d = 3.14159;

    /* Stampiamo il valore double */
    printf("Valore double: %f\n", u.d);

    /* Stampiamo il valore intero */
    printf("Valore intero: %d\n", u.i);

    return 0;
}

Se proviamo a compilare ed eseguire il programma otteniamo un risultato del genere:

Valore intero: 10000
Valore double: 3.141590
Valore intero: -266631570

Quello che è accaduto è che il valore intero 10000 è stato sovrascritto dal valore double 3.14159. In particolare, i 4 byte inferiori del double hanno sovrascritto i 4 byte dell'intero. Per cui, dopo l'assegnamento il valore intero è cambiato completamente.

Da questo punto di vista possiamo vedere una union come una struttura dati in grado di memorizzare un solo membro in maniera esclusiva.

Definizione

Accesso ai membri di una union

Per accedere ai membri di una union si procede nello stesso modo in cui si accede ai membri di una struct: con l'operatore punto .:

/* Accesso in scrittura */
nome_union.nome_membro = valore;

/* Accesso in lettura */
valore = nome_union.nome_membro;

In scrittura, quando si assegna un valore ad un membro di una union, viene sovrascritto il valore di tutti gli altri membri.

In lettura, se si accede ad un membro che non è stato assegnato, il risultato potrebbe non avere significato.

Dichiarazione di una union

Così come per le struct anche per le union possiamo dichiarare union etichettate o definire un nuovo tipo con typedef.

Ad esempio, possiamo dichiarare una union etichettata in questo modo:

union esempio {
    int i;
    double d;
    char c;
};

Successivamente possiamo dichiarare delle variabili union in questo modo:

union esempio u1;
union esempio u2;

Ovviamente, in tal caso dobbiamo sempre far precedere il nome dell'unione dalla parola chiave union.

Possiamo definire un nuovo tipo con typedef in questo modo:

typedef union {
    int i;
    double d;
    char c;
} esempio;

Fatto questo possiamo dichiarare delle variabili di questo tipo in questo modo:

esempio u1;
esempio u2;

In tal caso non è necessario far precedere il nome dell'unione dalla parola chiave union.

Definizione

Dichiarazione di una union

Per dichiarare una union possiamo farlo in due modi:

  1. Dichiarare una union etichettata:

    union nome_union {
        tipo1 nome1;
        tipo2 nome2;
        ...
        tipoN nomeN;
    };
    

    In tal caso, per dichiarare una variabile di tipo union dobbiamo far precedere il nome dell'unione dalla parola chiave union:

    union nome_union nome_variabile;
    
  2. Definire un nuovo tipo con typedef:

    typedef union {
        tipo1 nome1;
        tipo2 nome2;
        ...
        tipoN nomeN;
    } nome_tipo;
    

    In tal caso, per dichiarare una variabile di tipo union non è necessario far precedere il nome dell'unione dalla parola chiave union:

    nome_tipo nome_variabile;
    

Inizializzazione di una union

Una union può essere inizializzata analogamente a come viene inizializzata una struct.

Ad esempio, possiamo inizializzare una union in questo modo:

union {
    int i;
    double d;
    char c;
} u = { 10 };

In tal caso il valore viene assegnato al primo membro della union. In questo caso il primo membro è i che è un int e quindi il valore 10 viene assegnato a u.i.

Nello standard C99 è possibile usare un inizializzatore designato, per cui possiamo inizializzare un membro differente in questo modo:

union {
    int i;
    double d;
    char c;
} u = { .d = 3.14 };

In tal caso il valore viene assegnato al secondo membro della union, ossia u.d.

Definizione

Inizializzazione di una union

Una union può essere inizializzata utilizzando una lista di inizializzazione composta da un solo elemento:

union {
    tipo1 nome1;
    tipo2 nome2;
    ...
    tipoN nomeN;
} nome_variabile = { valore };

In tal caso il valore viene assegnato al primo membro della union.

A partire dal C99 è possibile utilizzare un inizializzatore designato per inizializzare un membro specifico:

union {
    tipo1 nome1;
    tipo2 nome2;
    ...
    tipoN nomeN;
} nome_variabile = { .nome_membro = valore };

In tal caso il valore viene assegnato al membro specificato.

Strutture Dati Miste

Uno degli usi più comuni delle union è quello di definire strutture dati miste. Una struttura dati mista è una struttura che può contenere diversi tipi di dati, ma solo uno alla volta. Questo è utile quando si vuole risparmiare spazio in memoria, poiché la struttura occupa solo lo spazio necessario per il tipo di dato attualmente in uso.

Prendiamo un esempio, supponiamo di voler realizzare un programma che gestisca un insieme di forme geometriche. Ogni forma geometrica può essere un cerchio, un rettangolo, un quadrato, un trapezio e così via. Ogni forma geometrica ha delle proprietà specifiche, ad esempio un cerchio ha un raggio, un rettangolo ha una base e un'altezza, un quadrato ha un lato, un trapezio ha due basi e un'altezza.

Se volessimo rappresentare queste forme geometriche con una struttura dati, dovremmo creare una struttura che contenga tutti i campi necessari per rappresentare ogni forma geometrica. Ad esempio, potremmo creare una struttura forma_geometrica con i seguenti campi:

#define CERCHIO 1
#define RETTANGOLO 2
#define QUADRATO 3
#define TRAPEZIO 4

struct forma_geometrica {
    int tipo;

    /* Se la forma è un cerchio */
    double raggio;

    /* Se la forma è un rettangolo */
    double base;
    double altezza;

    /* Se la forma è un quadrato */
    double lato;

    /* Se la forma è un trapezio */
    double base_maggiore;
    double base_minore;
    double altezza;
};

In questo esempio, abbiamo definito quattro costanti CERCHIO, RETTANGOLO, QUADRATO e TRAPEZIO per rappresentare i diversi tipi di forme geometriche. Abbiamo poi definito una struttura forma_geometrica che contiene un campo tipo per indicare il tipo di forma geometrica e i campi necessari per rappresentare ogni forma geometrica.

Tuttavia, questa soluzione ha un problema: ogni struttura forma_geometrica occupa spazio per tutti i campi, anche se alcuni di essi non sono utilizzati.

Ad esempio, se proviamo a creare un cerchio:

struct forma_geometrica cerchio = {CERCHIO, 5.0, 0.0, 0.0, 0.0, 0.0, 0.0};

La struttura cerchio occupa spazio per tutti i campi, anche se solo il campo raggio è utilizzato.

Per risolvere questo problema, possiamo definire una union all'interno della definizione della struttura forma_geometrica:

struct forma_geometrica {
    int tipo;

    union {
        struct {
            double raggio;
        } cerchio;

        struct {
            double altezza;
            double base;
        } rettangolo;

        struct {
            double lato;
        } quadrato;

        struct {
            double altezza;
            double base_maggiore;
            double base_minore;
        } trapezio;
    } dati;
};

In questo modo, la struttura forma_geometrica contiene un campo tipo per indicare il tipo di forma geometrica e una union dati che contiene le strutture necessarie per rappresentare ogni forma geometrica. Ogni struttura all'interno della union ha solo i campi necessari per rappresentare la forma geometrica corrispondente.

Adesso, se vogliamo provare a creare un cerchio, possiamo farlo in questo modo:

struct forma_geometrica cerchio = {
    .tipo = CERCHIO,
    .dati.cerchio = {5.0}
};

Ricapitolando:

Definizione

Strutture dati miste

Una Struttura Dati Mista è una struttura che può contenere diversi tipi di dati, ma solo uno alla volta. In C, è possibile realizzare una struttura dati di questo tipo usando una union all'interno di una struttura.

struct nome_struttura {
    int tipo;

    union {
        struct {
            tipo_campo1 campo1;
            tipo_campo2 campo2;
        } struttura1;

        struct {
            tipo_campo3 campo3;
        } struttura2;
    } dati;
};

Il campo tipo viene utilizzato per indicare il tipo di dato contenuto nella struttura. La union dati contiene le strutture necessarie per rappresentare i diversi tipi di dati.

L'esempio di sopra ci consente, inoltre, di osservare una particolare caratteristica delle union. Abbiamo detto, nella lezione precedente sulle union, che quando memorizziamo un valore in un campo della union e, successivamente, accediamo a un altro campo, il valore di tale campo non ha più senso.

Questo è sempre vero, tuttavia vi è un'eccezione. Quando i campi della union sono struct, come nel caso dell'esempio sopra, se i campi iniziali delle struct sono gli stessi, allora possiamo accedere a tali campi senza problemi.

Ad esempio, nella union di sopra, il campo rettangolo e il campo trapezio hanno lo stesso campo iniziale altezza. Per cui, se creiamo un rettangolo in questo modo:

struct forma_geometrica rettangolo = {
    .tipo = RETTANGOLO,
    .dati.rettangolo = {5.0, 10.0}
};

e, successivamente, accediamo al campo altezza ma attraverso il campo trapezio, il valore sarà corretto:

printf("Altezza del rettangolo: %.2f\n", rettangolo.dati.trapezio.altezza);

Questo è possibile perché i campi rettangolo e trapezio hanno lo stesso campo iniziale altezza.

Definizione

Eccezione all'accesso ai campi di una union

Quando i campi di una union sono struct e i campi iniziali delle struct sono gli stessi, il dato di tali campi iniziali continua ad essere valido purché si acceda a tali campi attraverso le struct con i campi iniziali comuni.

union {
    struct {
        tipo_comune campo_comune;
        tipo campo_specifico1;
    } struttura1;

    struct {
        tipo_comune campo_comune;
        tipo campo_specifico2;
    } struttura2;

    struct {
        tipo campo_specifico3;
    } struttura3;
};

L'accesso ai campi campo_comune attraverso struttura1 e struttura2 è equivalente.

Il campo etichetta o discriminante

Nell'esempio di sopra sulle forme geometriche, abbiamo aggiunto un campo tipo che indica il tipo di forma geometrica.

L'aggiunta di questo campo risulta necessaria, in quanto le union hanno il difetto che non sappiamo quale sia stato l'ultimo campo scritto. Per tal motivo, si utilizza questo campo che prende anche il nome di discriminante o campo etichetta.

Ovviamente, è sempre responsabilità del programma gestire correttamente il campo tipo e garantire che sia coerente con i dati presenti nella union.

Ad esempio, se vogliamo realizzare una funzione stampa_forma che stampi le informazioni di una forma geometrica, possiamo farlo in questo modo:

void stampa_forma(struct forma_geometrica forma) {
    switch (forma.tipo) {
        case CERCHIO:
            printf("Cerchio di raggio %.2f\n",
                    forma.dati.cerchio.raggio);
            break;
        case RETTANGOLO:
            printf("Rettangolo di base %.2f e altezza %.2f\n",
                    forma.dati.rettangolo.base,
                    forma.dati.rettangolo.altezza);
            break;
        case QUADRATO:
            printf("Quadrato di lato %.2f\n",
                    forma.dati.quadrato.lato);
            break;
        case TRAPEZIO:
            printf("Trapezio di base maggiore %.2f, base minore %.2f e altezza %.2f\n",
                    forma.dati.trapezio.base_maggiore,
                    forma.dati.trapezio.base_minore,
                    forma.dati.trapezio.altezza);
            break;
        default:
            printf("Forma sconosciuta\n");
            break;
    }
}

In questa funzione, utilizziamo un'istruzione switch per stampare le informazioni della forma geometrica in base al campo tipo.

Definizione

Campo Etichetta o Discriminante

Il Campo Etichetta o Discriminante è un campo di una struttura dati che indica il tipo di dato contenuto nella struttura dati mista. È utilizzato per distinguere i diversi tipi di dati contenuti nella struttura.

struct nome_struttura {
    int tipo;

    union {
        struct {
            tipo_campo1 campo1;
            tipo_campo2 campo2;
        } struttura1;

        struct {
            tipo_campo3 campo3;
        } struttura2;
    } dati;
};

Il campo tipo viene utilizzato come discriminante per indicare il tipo di dato contenuto nella struttura.

Nota

Gestione del Campo Etichetta

È responsabilità del programma gestire correttamente il campo etichetta e garantire che sia coerente con i dati presenti nella struttura dati mista.

Quindi, ogniqualvolta si modifichi il contenuto della union, è necessario aggiornare anche il campo etichetta.

In Sintesi

In questo articolo abbiamo visto come funzionano le union in linguaggio C. In particolare abbiamo visto che le union possono contenere elementi di tipi differenti, ma in un qualunque istante il contenuto della union è rappresentato dall'ultimo membro assegnato.

Abbiamo visto le similitudini a livello sintattico che una union ha rispetto ad una struct:

  • possiamo dichiarare una union etichettata o definire un nuovo tipo con typedef
  • l'accesso ai membri di una union avviene con l'operatore punto .
  • possiamo inizializzare una union in modo analogo a come viene inizializzata una struct

Un'importante applicazione delle union, che abbiamo studiato in questa lezione, è la possibilità di definire strutture dati miste. Queste strutture dati sono utili quando si vuole risparmiare spazio in memoria, poiché occupano solo lo spazio necessario per il tipo di dato attualmente in uso.

Inoltre, abbiamo visto come utilizzare un campo etichetta o discriminante per distinguere i diversi tipi di dati contenuti in una struttura dati mista.