Enumerazioni in Linguaggio C

Un importante costrutto del linguaggio C sono le enumerazioni. Queste ci permettono di definire un nuovo tipo di dato che può assumere solo valori definiti all'interno di un elenco. In questo modo, possiamo creare variabili che possono assumere solo valori specifici.

In questa lezione vedremo come definire e usare i tipi enumerati in linguaggio C. Vedremo, inoltre, quale sia la loro relazione con gli interi e come possiamo usarli per creare strutture dati miste.

Enumerazioni

Spesso, nei nostri programmi, sorge l'esigenza di dichiarare variabili che possono assumere valori soltanto in un ristretto insieme. Si pensi, ad esempio, alle variabili booleane, il cui scopo è quello di memorizzare uno tra i due possibili valori vero o falso. Un altro esempio potrebbe essere quello di memorizzare i giorni della settimana, i mesi, i semi delle carte da gioco, ecc.

In tal caso, una delle possibili scelte può essere quella di usare variabili di tipo intero, assegnando ad ogni valore un significato particolare. Ad esempio, volendo memorizzare il giorno della settimana, si potrebbe assegnare il valore 1 al lunedì, 2 al martedì, ecc. Potremmo scrivere codice come quello che segue:

int giorno; /* Memorizza il giorno della settimana */

giorno = 3; /* 3 = mercoledì */

Questo codice funziona. Tuttavia, è difficile da leggere e da mantenere. A prima vista, infatti, non si capisce subito quale sia l'intervallo dei valori che la variabile può assumere. Si tratta del giorno della settimana, per cui i valori vanno da 1 a 7, oppure del giorno del mese? Il secondo problema è che non risulta subito evidente l'associazione del valore numerico al giorno della settimana. In questo caso 3 vale mercoledì, ma non è immediato. Infine, potremmo, per errore, assegnare alla variabile un valore non valido, come 8 o 0. In tal caso, il compilatore non ci segnalerà alcun errore, in quanto non è un errore di sintassi, ma di logica.

Una prima soluzione potrebbe essere quella di usare delle macro semplici per definire tipo e giorni:

#define GIORNO_DELLA_SETTIMANA int

#define LUNEDI 1
#define MARTEDI 2
#define MERCOLEDI 3
#define GIOVEDI 4
#define VENERDI 5
#define SABATO 6
#define DOMENICA 7

GIORNO_DELLA_SETTIMANA giorno;

giorno = MERCOLEDI;

Sebbene sia una soluzione valida, non si tratta di quella ottimale per due motivi.

In primo luogo, le macro non sono altro che sostituzioni di testo. Questo significa che il compilatore sostituirà il testo della macro con il suo valore ovunque essa venga usata. Per cui, in fase di debug, non vedremo mai il tipo GIORNO_DELLA_SETTIMANA ma solo int.

Inoltre, da nessuna parte è indicato che le macro da LUNEDI a DOMENICA afferiscono allo stesso tipo. Si tratta, semplicemente, di un elenco di costanti numeriche.

Per sopperire a queste problematiche, il linguaggio C fornisce i tipi enumerati.

Un tipo enumerato è un particolare tipo del linguaggio C per cui si elencano (si enumerano) i possibili valori. Lo sviluppatore deve definire sia il nome del tipo enumerato che dare un'**etichetta a tutti i possibili valori.

Ad esempio, volendo definire un tipo enumerato per i giorni della settimana, possiamo scrivere:

enum {
    LUNEDI,
    MARTEDI,
    MERCOLEDI,
    GIOVEDI,
    VENERDI,
    SABATO,
    DOMENICA
} giorno;

giorno = MERCOLEDI;

La variabile giorno è una variabile enum e può assumere esclusivamente i valori definiti nell'elenco.

Definizione

Tipi Enumerati

I tipi enumerati sono un particolare tipo di dato del linguaggio C le cui variabili possono assumere solo valori definiti all'interno di un elenco o insieme.

La sintassi per definire una variabile enumerata è la seguente:

enum {
    VALORE_1,
    VALORE_2,
    ...
    VALORE_N
} nome_variabile;

Sebbene non abbiano nulla in comune con le variabili struct o union, le variabili enum vengono dichiarate in maniera molto simile. La differenza è che le costanti presenti nell'insieme devono avere un nome differente da qualsiasi identificatore allo stesso livello di scope. Ad esempio, il codice che segue non è valido:

enum {
    LUNEDI,
    MARTEDI,
    MERCOLEDI,
    GIOVEDI,
    VENERDI,
    SABATO,
    DOMENICA
} giorno;

int LUNEDI = 1; /* Errore: LUNEDI è già definito */

Inoltre, le costanti definite all'interno di un enum devono seguire le stesse regole di visibilità delle variabili. Ad esempio, il codice che segue non è valido:

int funzione1() {
    enum {
        LUNEDI,
        MARTEDI,
        MERCOLEDI,
        GIOVEDI,
        VENERDI,
        SABATO,
        DOMENICA
    } giorno;

    /* ... */
}

int funzione2() {
    int giorno = LUNEDI; /* Errore: LUNEDI non è definito */
}

In questo caso, la costante LUNEDI non è visibile all'interno di funzione2. Essa è visibile solo all'interno di funzione1.

Enumerazioni etichettate e typedef

Nell'esempio precedente, abbiamo definito un tipo enumerato anonimo. Questo significa che il tipo enum non ha un nome. Ciò rappresenta un problema nel caso in cui, in più punti del nostro codice, vogliamo dichiarare variabili dello stesso tipo enumerato.

Per ovviare a questo problema, così come nel caso delle struct e delle union, possiamo creare delle enumerazioni etichettate. Queste sono delle definizioni di tipo che associano un nome ad un tipo enumerato. Ad esempio, possiamo scrivere:

enum giorno_settimana {
    LUNEDI,
    MARTEDI,
    MERCOLEDI,
    GIOVEDI,
    VENERDI,
    SABATO,
    DOMENICA
};

enum giorno_settimana giorno;

giorno = MERCOLEDI;

In questo caso, giorno_settimana è il nome del tipo enumerato. La variabile giorno è di tipo enum giorno_settimana.

Per dichiarare una variabile di questo tipo dobbiamo sempre far precedere il nome del tipo enumerato dalla parola chiave enum.

Un'altra soluzione è quella di usare il costrutto typedef per creare un alias del tipo enumerato. Ad esempio, possiamo scrivere:

typedef enum {
    LUNEDI,
    MARTEDI,
    MERCOLEDI,
    GIOVEDI,
    VENERDI,
    SABATO,
    DOMENICA
} giorno_settimana;

giorno_settimana giorno;

giorno = MERCOLEDI;

In questo caso, giorno_settimana è un alias per il tipo enumerato anonimo definito subito dopo la parola chiave enum.

Definizione

Definizione di Tipi Enumerati

In linguaggio C, per assegnare un nome ad un tipo enumerato, si può procedere in due modi:

  1. Definire un tipo enumerato etichettato:

    enum nome_tipo {
        VALORE_1,
        VALORE_2,
        ...
        VALORE_N
    };
    

    In tal caso, per dichiarare una variabile di quel tipo, si dovrà scrivere enum nome_tipo nome_variabile.

  2. Usare typedef per creare un alias del tipo enumerato:

    typedef enum {
        VALORE_1,
        VALORE_2,
        ...
        VALORE_N
    } nome_tipo;
    

    In tal caso, per dichiarare una variabile di quel tipo, si dovrà scrivere nome_tipo nome_variabile.

Relazione tra Enumerati e Interi

Da un punto di vista semantico, un tipo enumerato rappresenta un nuovo tipo in tutto e per tutto.

Internamente, tuttavia, il linguaggio C considera tutti i tipi enumerati come interi.

Torniamo all'esempio dei giorni della settimana:

enum giorno_settimana {
    LUNEDI,
    MARTEDI,
    MERCOLEDI,
    GIOVEDI,
    VENERDI,
    SABATO,
    DOMENICA
};

enum giorno_settimana giorno;

giorno = GIOVEDI;

Il compilatore, internamente, trasforma le costanti LUNEDI, MARTEDI, ecc. in valori interi. In particolare, il compilatore parte da 0 e assegna un valore crescente ad ogni costante. Quindi, LUNEDI sarà 0, MARTEDI sarà 1, ecc.

Analogamente, la variabile giorno internamente conterrà il valore 4 che corrisponde a GIOVEDI.

Questa corrispondenza tra enumerati e interi arriva fino al punto in cui possiamo assegnare ad una variabile di tipo enumerato un valore intero e viceversa. In base a ciò, il codice che segue è perfettamente legale:

enum giorno_settimana giorno;

giorno = 4; /* GIOVEDI */

int giorno_intero = GIOVEDI;
Definizione

I tipi Enumerati sono Interi

In linguaggio C, i tipi enumerati sono considerati come interi. Questo significa che è possibile assegnare ad una variabile di tipo enumerato un valore intero e viceversa.

Inoltre, le costanti definite all'interno di un tipo enumerato sono considerate come costanti intere a partire da 0.

enum {
    VALORE_1,       /* 0 */
    VALORE_2,       /* 1 */
    ...
    VALORE_N        /* N - 1 */
} nome_variabile;

L'assegnazione di valori interi alle costanti di un tipo enumerato può essere modificato in maniera diretta.

Supponiamo, ad esempio, di voler assegnare alla costante LUNEDI il valore 1, a MARTEDI il valore 2, ecc. Possiamo scrivere:

enum giorno_settimana {
    LUNEDI = 1,
    MARTEDI = 2,
    MERCOLEDI = 3,
    GIOVEDI = 4,
    VENERDI = 5,
    SABATO = 6,
    DOMENICA = 7
};

In questo modo abbiamo esplicitato i valori da assegnare alle costanti. Inoltre, possiamo assegnare valori non consecutivi:

enum giorno_settimana {
    LUNEDI = 1,
    MARTEDI = 3,
    MERCOLEDI = 5,
    GIOVEDI = 7,
    VENERDI = 9,
    SABATO = 11,
    DOMENICA = 13
};

Non ci sono vincoli sui valori che le costanti dell'enumerato possono assumere. Possiamo, infatti, assegnare anche gli stessi valori a due o più costanti. Ciò risulta utile quando vogliamo creare costanti che siano alias l'una dell'altra.

Ad esempio, potremmo creare il nostro enumerato dei giorni della settimana in maniera tale che abbiamo costanti sia in inglese che italiano:

enum giorno_settimana {
    LUNEDI = 1,
    MONDAY = 1,
    MARTEDI = 2,
    TUESDAY = 2,
    MERCOLEDI = 3,
    WEDNESDAY = 3,
    GIOVEDI = 4,
    THURSDAY = 4,
    VENERDI = 5,
    FRIDAY = 5,
    SABATO = 6,
    SATURDAY = 6,
    DOMENICA = 7,
    SUNDAY = 7
};

In questo modo, possiamo usare indifferentemente LUNEDI o MONDAY per riferirci al primo giorno della settimana.

Definizione

Assegnazione di Valori alle Costanti di un Enumerato

In linguaggio C, è possibile assegnare valori alle costanti di un tipo enumerato. Questo può essere fatto in maniera diretta:

enum {
    COSTANTE_1 = 1,
    COSTANTE_2 = 2,
    COSTANTE_3 = 3,
    ...
    COSTANTE_N = N
};

L'assegnazione di valori alle costanti di un tipo enumerato può essere fatta anche in maniera parziale. Così facendo, il comportamento del compilatore è quello di assegnare automaticamente i valori successivi a partire dall'ultimo valore assegnato. Ad esempio:

enum giorno_settimana {
    LUNEDI = 1,
    MARTEDI,        /* 2 */
    MERCOLEDI,      /* 3 */
    GIOVEDI = 10,
    VENERDI,       /* 11 */
    SABATO,        /* 12 */
    DOMENICA       /* 13 */
};

Nell'esempio, quello che accade è che il compilatore assegna 1 a LUNEDI; successivamente, dato che a MARTEDI non è stato assegnato alcun valore, il compilatore assegna 2, cioè il valore immediatamente successivo all'ultimo valore assegnato. Lo stesso vale per MERCOLEDI. A GIOVEDI viene assegnato 10 e, da quel punto in poi, il compilatore assegna i valori successivi.

Definizione

Assegnazione di Valori Parziali alle Costanti di un Enumerato

In linguaggio C, è possibile assegnare valori parziali alle costanti di un tipo enumerato. In tal caso, il compilatore assegnerà automaticamente i valori successivi a partire dall'ultimo valore assegnato.

enum {
    COSTANTE_1 = 1,
    COSTANTE_2,     /* 2 */
    COSTANTE_3,     /* 3 */

    ...

    COSTANTE_N      /* N */
};
Nota

Evitare Assegnazioni Parziali

Il nostro consiglio sullo stile da adottare nell'assegnare valori alle costanti di un tipo enumerato è quello di evitare assegnazioni parziali. In altre parole, o si lasciano tutte le costanti senza valore, oppure si assegnano valori a tutte le costanti.

Questo perché, in caso di assegnazioni parziali, potremmo introdurre errori difficili da individuare.

Essendo i tipi enumerati, sostanzialmente, degli interi nascosti, possiamo effettuare su di essi tutte le operazioni tipiche degli interi.

Ad esempio:

enum giorno_settimana giorno = LUNEDI;

giorno++; /* MARTEDI */

La grande potenza sta anche nel fatto che possiamo usare gli enumerati in un'istruzione switch o in un'espressione condizionale if. Ad esempio, possiamo creare una semplice funzione di stampa del giorno con uno switch:

void stampa_giorno(enum giorno_settimana giorno) {
    switch (giorno) {
        case LUNEDI:
            printf("Lunedì\n");
            break;
        case MARTEDI:
            printf("Martedì\n");
            break;
        case MERCOLEDI:
            printf("Mercoledì\n");
            break;
        case GIOVEDI:
            printf("Giovedì\n");
            break;
        case VENERDI:
            printf("Venerdì\n");
            break;
        case SABATO:
            printf("Sabato\n");
            break;
        case DOMENICA:
            printf("Domenica\n");
            break;
        default:
            printf("Giorno non valido\n");
    }
}

Enumerati come determinanti di una Struttura Mista

Nella lezione precedente abbiamo studiato le union e abbiamo visto come usarle per costruire Strutture Dati Miste. Queste sono strutture che contengono campi di diversi tipi e che possono essere usate per rappresentare dati eterogenei.

Nell'esempio della lezione abbiamo creato una struttura dati mista che rappresentava diverse figure geometriche: cerchi, rettangoli, quadrati e trapezi. La struttura risultante aveva questa forma:

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

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 esempio, abbiamo introdotto nella struttura dati un campo di nome tipo che funge da discriminante per il tipo di dato memorizzato. Questo campo ci permette di capire quale dei campi della union è attivo in un dato momento.

In questi casi, piuttosto che usare degli interi, possiamo usare dei tipi enumerati che si prestano meglio allo scopo. Questo rende il codice più leggibile e più sicuro. Ad esempio, possiamo scrivere:

enum tipo_forma {
    CERCHIO,
    RETTANGOLO,
    QUADRATO,
    TRAPEZIO
};

struct forma_geometrica {
    enum tipo_forma 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, il campo tipo della struttura forma_geometrica può assumere solo i valori definiti nell'enumerato tipo_forma.

In Sintesi

In questa lezione abbiamo studiato i tipi enumerati del linguaggio C.

Una variabile di tipo enumerato può assumere solo i valori definiti all'interno di un elenco. Ossia, i possibili valori che può assumere sono enumerati tra parentesi.

I tipi enumerati sono considerati come interi dal compilatore. Questo significa che possiamo assegnare ad una variabile di tipo enumerato un valore intero e viceversa. Inoltre, possiamo usare gli enumerati in tutte le operazioni tipiche degli interi.

Infine, abbiamo visto come gli enumerati possano essere usati come determinanti di una struttura dati mista. In tal caso, gli enumerati ci permettono di capire quale campo della union è attivo in un dato momento.