[successivo] [precedente] [inizio] [fine] [indice generale] [indice ridotto] [indice analitico] [home] [volume] [parte]


Capitolo 579.   C: struttura, unione, campo, enumerazione, costante composta

Fino a questo punto sono stati incontrati solo i tipi di dati primitivi, oltre agli array di questi (incluse le stringhe). Nel linguaggio C, come in altri, è possibile definire dei tipi di dati aggiuntivi, derivati dai tipi primitivi.

Nel capitolo si usa la convenzione di nominare le strutture, le unioni e le enumerazioni con un'iniziale maiuscola. Per quanto riguarda invece i tipi di dati derivati, ottenuti con l'istruzione typedef, si segue l'uso comune di aggiungere l'estensione _t.

579.1   Enumerazioni

È possibile dichiarare una variabile di tipo enumerativo, costituita tecnicamente da un intero, la quale può rappresentare solo un insieme prestabilito di valori, indicati simbolicamente attraverso delle definizioni. I valori simbolici che possono essere rappresentati sono tradotti in un numero intero, ma il programmatore non dovrebbe avere la necessità di avere a che fare direttamente con tali valori numerici corrispondenti. In altri termini, il tipo enumerativo è una forma di rappresentazione di un intero attraverso costanti mnemoniche.

enum nome { costante[, costante]...}

La sintassi indicata mostra il modo in cui si definisce un tipo del genere: all'interno di parentesi graffe si elencano i nomi delle costanti che possono essere assegnate a una variabile di questo tipo. Tuttavia, alle costanti si può associare un valore intero in modo esplicito; pertanto, la costante può essere espressa così:

nome_simbolico[=n]

Si osservi l'esempio seguente che comunque non rappresenta un programma completo:

...
enum Colore { nero, marrone, rosso, arancio, giallo, verde, blu,
              viola, grigio, bianco, argento=100, oro };
...
enum Colore c;          // Dichiara la variabile «c».
...
c = marrone + 1;        // Assegna a «c» il valore successivo a
                        // «marrone»; in pratica assegna il valore
                        // «rosso».
...
if (c <= rosso);                        // Se il colore va dal nero
  {                                     // al rosso, visualizza un
    printf ("Non mi piace: %d\n", c);   // messaggio e mostra anche
  }                                     // il numero corrispondente.
...

All'inizio viene dichiarato il tipo enumerativo Colore, come insieme di colori principali, definiti simbolicamente per nome. Va osservato che nel caso dell'argento, viene associato espressamente il valore 100.

In mancanza di associazioni esplicite tra il valore simbolico e valore numerico, il compilatore associa al primo dei simboli il valore zero e dà a quelli successivi un numero ottenuto incrementando di una unità quello precedente. Nel caso dell'esempio, nero corrisponde a zero, marrone a uno, rosso a due e così di seguito fino al bianco. Il colore argento è definito espressamente (quindi dal nove del bianco si salta al 100 dell'argento) e il colore dell'oro viene determinato implicitamente come pari a argento+1, ovvero uguale a 101.

Seguendo l'esempio si vede la dichiarazione della variabile c di tipo enum Colore. In pratica, viene dichiarata una variabile di tipo intero, in grado di contenere i valori dell'enumerazione Colore.

Successivamente si assegna alla variabile c la somma tra la costante marrone (pari a uno) e il numero uno. In pratica si assegna il valore due, ma in base al contesto si intende di avere assegnato rosso.

Alla fine dell'esempio si vede un confronto tra la variabile c e un colore di quelli definiti simbolicamente. Di fatto si sta confrontando il valore della variabile con il numero due, ma in pratica sembra di valutare la cosa solo sul piano della sequenza ideale che è stata attribuita a quei colori.

La dichiarazione di una variabile enumerativa coincide quindi con la dichiarazione di un insieme di costanti simboliche, le quali non possono essere ridefinite. Pertanto, non è possibile dichiarare due variabili diverse che condividono costanti simboliche con lo stesso nome, a meno di essere in un campo di azione differente:

:-(

enum Colori { nero, marrone, rosso, arancio, giallo, verde, blu,
              viola, grigio, bianco };

enum Bianco_e_nero { nero, bianco };    // Non si può.

Le costanti simboliche definite attraverso le enumerazioni, possono essere usate anche al di fuori delle variabili dichiarate espressamente per questo scopo, purché possano ragionevolmente contenerne il valore. È anche evidente che al posto delle enumerazioni definite in questo modo sia possibile gestire direttamente le costanti. L'esempio seguente riporta i passi equivalenti di quanto già visto all'inizio della sezione:

...
const int nero    = 0;
const int marrone = 1;
const int rosso   = 2;
const int arancio = 3;
const int giallo  = 4;
const int verde   = 5;
const int blu     = 6;
const int viola   = 7;
const int grigio  = 8;
const int bianco  = 9;
const int argento = 100;
const int oro     = 101;
...
int c;                  // Dichiara la variabile «c».
...
c = marrone + 1;        // Assegna a «c» il valore successivo a
                        // «marrone»; in pratica assegna il valore
                        // «rosso».
...
if (c <= rosso);                        // Se il colore va dal nero
  {                                     // al rosso, visualizza un
    printf ("Non mi piace: %d\n", c);   // messaggio e mostra anche
  }                                     // il numero corrispondente.
...

579.2   Strutture

Gli array sono una serie di elementi uguali, tutti adiacenti nel modello di rappresentazione della memoria, ideale o reale che sia. In modo simile si possono definire strutture di dati più complesse in cui gli elementi adiacenti siano di tipo differente. Gli elementi che compongono una struttura sono i suoi membri. In pratica, una struttura è una sorta di mappa di accesso a un'area di memoria, attraverso i suoi membri.

La variabile contenente una struttura si comporta in modo analogo alle variabili di tipo primitivo, per cui, la variabile che è stata creata a partire da una struttura, rappresenta tutta la zona di memoria occupata dalla struttura stessa e non solo il riferimento al suo inizio. Questa distinzione è importante, per non fare confusione con il comportamento relativo agli array che sono sostanzialmente solo dei puntatori.

La dichiarazione di una struttura si articola in due fasi: la dichiarazione del tipo e la dichiarazione delle variabili che utilizzano quella struttura.

struct Datario { int giorno; int mese; int anno; };

L'esempio mostra la dichiarazione della struttura Datario (ovvero del tipo struct Datario) composta da tre interi dedicati a contenere rispettivamente: il giorno, il mese e l'anno. In questo caso, trattandosi di tre elementi dello stesso tipo, sarebbe stato possibile utilizzare un array, ma come è possibile vedere in seguito, una struttura può essere conveniente anche in queste situazioni.

È importante osservare che le parentesi graffe sono parte dell'istruzione di dichiarazione della struttura e non rappresentano un blocco di istruzioni. Per questo motivo appare il punto e virgola finale, cosa che potrebbe sembrare strana, specialmente quando la struttura si articola su più righe come nell'esempio seguente:

struct Datario {
    int giorno;
    int mese;
    int anno;
};              // Il punto e virgole finale è necessario.

La dichiarazione delle variabili che utilizzano la struttura può avvenire contestualmente con la dichiarazione della struttura, oppure in un momento successivo. L'esempio seguente mostra la dichiarazione del tipo struct Datario, seguito da un elenco di variabili che utilizzano quel tipo: inizio e fine.

struct Datario {
    int giorno;
    int mese;
    int anno;
} inizio, fine;

Tuttavia, il modo più elegante per dichiarare delle variabili a partire da una struttura è quello seguente:

struct Datario inizio, fine;

Quando una variabile è stata definita come organizzata secondo una certa struttura, si accede ai suoi componenti attraverso l'indicazione del nome della variabile stessa, seguita dall'operatore punto (.) e dal nome dell'elemento particolare.

inizio.giorno = 1;
inizio.mese = 1;
inizio.anno = 2007;
...
fine.giorno = inizio.giorno;
fine.mese = inizio.mese +1;
fine.anno = inizio.anno;

Una struttura può essere dichiarata in modo anonimo, definendo immediatamente tutte le variabili che fanno uso di quella struttura. La differenza sta nel fatto che la struttura non viene nominata nel momento della dichiarazione e, dopo la definizione dei suoi elementi, devono essere elencate tutte le variabili in questione. Evidentemente, non c'è la possibilità di riutilizzare questa struttura per altre variabili definite in un altro punto, ma soprattutto, come viene mostrato in seguito, diventa impossibile indicare il tipo di struttura come parametro formale di una funzione.

:-(

struct {
    int giorno;
    int mese;
    int anno;
} inizio, fine;

579.3   Assegnamento, inizializzazione, campo di azione e puntatori delle strutture

Nella sezione precedente si è visto come accedere ai vari componenti della struttura, attraverso una notazione che utilizza l'operatore punto. Volendo è possibile assegnare a una variabile di questo tipo l'intero contenuto di un'altra che appartiene alla stessa struttura:

inizio.giorno = 1;
inizio.mese = 1;
inizio.anno = 2007
...
fine = inizio;
fine.mese++;

L'esempio mostra l'assegnamento alla variabile fine di tutta la variabile inizio. Questo è ammissibile solo perché si tratta di variabili dello stesso tipo, cioè di strutture di tipo Datario (come deriva dagli esempi precedenti). Se invece si trattasse di variabili costruite a partire da strutture differenti, anche se realizzate nello stesso modo, con gli stessi membri, ciò non sarebbe ammissibile.

...
struct Datario {int giorno; int mese; int anno;};
struct Giorno  {int giorno; int mese; int anno;};
...
struct Datario ingresso = {31, 12, 2007};
struct Giorno  uscita;
uscita = ingresso;      // Errore: i dati sono incompatibili
...

Nel momento della dichiarazione di una struttura, è possibile anche inizializzarla utilizzando una forma simile a quella disponibile per gli array:

struct Datario inizio = { 1, 1, 2007 };

Oppure, per essere precisi e non dipendere dall'ordine dei campi nella struttura:

struct Datario inizio = { .giorno=1, .mese=1, .anno=2007 };

Dal momento che le strutture sono tipi di dati nuovi, per poterne fare uso occorre che la dichiarazione relativa sia accessibile a tutte le parti del programma che hanno bisogno di accedervi. Probabilmente, il luogo più adatto è al di fuori delle funzioni, eventualmente anche in un file di intestazione realizzato appositamente.

Ciò dovrebbe bastare a comprendere che le variabili che contengono una struttura vengono passate regolarmente attraverso le funzioni, purché la dichiarazione del tipo corrispondente sia precedente ed esterno alla descrizione delle funzioni stesse.

...
struct Datario { int giorno; int mese; int anno; };
...
void elabora (struct Datario oggi)
{
    ...
}

L'esempio seguente che rappresenta un programma completo, serve a dimostrare che, nella chiamata di una funzione, la struttura viene passata per valore (e non per riferimento come avviene con gli array):

#include <stdio.h>

struct Datario {int giorno; int mese; int anno;};

void f (struct Datario d)
{
    unsigned int indirizzo = (int) &d;
    d.giorno = 28;
    d.mese = 2;
    d.anno = 2007;
    printf ("data %d-%d-%d inserita all'indirizzo %u\n",
             d.giorno, d.mese, d.anno, indirizzo);
}

int main (void)
{
    struct Datario data = {31, 12, 2007};
    unsigned int ind = (int) &data;
    f (data);
    printf ("data %d-%d-%d inserita all'indirizzo %u\n",
             data.giorno, data.mese, data.anno, ind);
    return 0;
}

Se si esegue il programma si ottiene un messaggio simile a quello seguente, dove si vede che gli l'indirizzi delle variabili contenenti la struttura, prima della chiamata della funzione e all'interno della stessa, sono differenti:

data 28-2-2007 inserita all'indirizzo 3212916960
data 31-12-2007 inserita all'indirizzo 3212916992

D'altro canto, se la variabile fosse la stessa, le modifiche fatte all'interno della funzione sarebbero visibili anche dopo la chiamata.

Così come nel caso dei tipi primitivi, anche con le strutture si possono creare dei puntatori. La loro dichiarazione avviene in modo intuitivo, come nell'esempio seguente:

struct Datario *p_data_fattura;
...
p_data_fattura = &inizio;
...

Quando si utilizza un puntatore a una struttura, diventa un po' più difficile fare riferimento ai vari componenti della struttura stessa, perché l'operatore punto (.) che serve a unire il nome della struttura a quello dell'elemento, ha priorità rispetto all'asterisco che si utilizza per dereferenziare il puntatore:

:-(

*p_data_fattura.giorno = 15; // Non è valido!

L'esempio appena mostrato, non è ciò che sembra, perché l'asterisco posto davanti viene valutato dopo l'elemento p_data_fattura.giorno, il quale non esiste. Per risolvere il problema si possono usare le parentesi, come nell'esempio seguente:

:-(

(*p_data_fattura).giorno = 15; // Corretto.

In alternativa si può usare l'operatore ->, fatto espressamente per i puntatori a una struttura:

:-)

p_data_fattura->giorno = 15; // Corretto.

L'esempio seguente è una variante di quello già presentato in precedenza per dimostrare il passaggio per valore delle variabili che contengono una struttura. Ma in questo caso, il passaggio dei dati avviene esplicitamente per riferimento:

#include <stdio.h>

struct Datario {int giorno; int mese; int anno;};

void f (struct Datario *d)
{
    unsigned int indirizzo = (int) d;
    d->giorno = 28;
    d->mese = 2;
    d->anno = 2007;
    printf ("data %d-%d-%d inserita all'indirizzo %u\n",
             d->giorno, d->mese, d->anno, indirizzo);
}

int main (void)
{
    struct Datario data = {31, 12, 2007};
    unsigned int ind = (int) &data;
    f (&data);
    printf ("data %d-%d-%d inserita all'indirizzo %u\n",
             data.giorno, data.mese, data.anno, ind);
    return 0;
}

In tal caso, gli indirizzi della struttura appaiono uguali e le modifiche applicate all'interno della funzione si riflettono nella variabile originale:

data 28-2-2007 inserita all'indirizzo 3214580384
data 28-2-2007 inserita all'indirizzo 3214580384

579.4   Scostamento all'interno delle strutture

Il file strdef.h definisce una macro-istruzione che, attraverso la parvenza di una funzione, consente di misurare lo scostamento di un membro della struttura, rispetto all'inizio della stessa:

offsetof (tipo, membro)

Si osservi l'esempio seguente:

#include <stdio.h>
#include <stddef.h>

struct Elenco {
    char  uno;
    short due;
    int   tre;
};

int main (int argc, char *argv[])
{
    size_t offset = offsetof (struct Elenco, due);
    printf ("Il membro \"due\" si trova %d byte dopo "
            "l'inizio della struttura.\n", offset);
    return 0;
}

Come si può vedere, la macro-istruzione offsetof produce un risultato di tipo size_t. Supponendo che il compilatore allinei i membri della struttura secondo multipli di due byte, il messaggio emesso dal programma potrebbe essere così:

Il membro "due" si trova 2 byte dopo l'inizio della struttura.

Pertanto, in questo caso, dopo il membro uno c'è un byte inutilizzato prima del membro due.

È il caso di ribadire che offsetof è una macro-istruzione, ottenuta tramite le funzionalità del precompilatore. Diversamente, è probabile che sia impossibile realizzare una funzione che si comporti nello stesso modo apparente.

579.5   Unioni

L'unione permette di definire un tipo di dati accessibile in modi diversi, gestendolo come se si trattasse contemporaneamente di tipi differenti. La dichiarazione è simile a quella della struttura; quello che bisogna tenere a mente è che si fa riferimento alla stessa area di memoria; pertanto, lo spazio occupato è pari a quello del membro più grande.

union Livello {
    char c;
    int  i;
};

Si immagini, per esempio, di voler utilizzare indifferentemente una serie di lettere alfabetiche, oppure una serie di numeri, per definire un livello di qualcosa («A» equivalente a uno, «B» equivalente a due, ecc.). Le variabili generate a partire da questa unione, possono essere gestite nei modi stabiliti, come se fossero una struttura, ma condividendo la stessa area di memoria.

union Livello carburante;

L'esempio mostra in che modo si possa dichiarare una variabile di tipo union Livello, riferita all'omonima unione. Il bello delle unioni sta però nella possibilità di combinarle con le strutture.

struct Livello {
    char tipo;
    union {
        char c;         // Usato se tipo == 'c'.
        int  i;         // Usato se tipo == 'n'.
    };
};

L'esempio non ha un grande significato pratico, ma serve a chiarire le possibiltà. La variabile tipo serve ad annotare il tipo di informazione contenuta nell'unione, se di tipo carattere o numerico. L'unione viene dichiarata in modo anonimo come appartenente alla struttura.

L'esempio successivo, che è completo, permette di verificare l'ordine con cui vengono memorizzati i byte in memoria. L'unione dichiarata parte dal presupposto che un numero short int utilizzi l'equivalente di due caratteri:

#include <stdio.h>

union Little_big {
    short int i;        // 16 bit
    char c[2];          // 8 bit, 8 bit
};

int main (void)
{
    union Little_big lb;
    lb.i = 0x1234;
    printf ("%x %x%x\n", lb.i, lb.c[0], lb.c[1]);
    return 0;
}

Eseguendo il programma in un'elaboratore con architettura little endian si ottiene il risultato seguente:

1234 3412

579.6   Campi

All'interno di una struttura è possibile definire l'accesso a ogni singolo bit di un tipo di dati determinato, oppure a gruppetti di bit. In pratica viene dato un nome a ogni bit o gruppetto.

struct Luci {
    unsigned char
        b0      :1,
        b1      :1,
        b2      :1,
        b3      :1,
        b4      :1,
        b5      :1,
        b6      :1,
        b7      :1,
};

L'esempio mostra l'abbinamento di otto nomi ai bit di un tipo char. Il primo, b0, rappresenta il bit più a destra, ovvero quello meno significativo. Se il tipo char occupasse una dimensione maggiore di 8 bit, la parte eccedente verrebbe semplicemente sprecata.

struct Luci salotto;
...
salotto.b2 = 1;

L'esempio mostra la dichiarazione della variabile salotto come appartenente alla struttura mostrata sopra, quindi l'assegnamento del terzo bit a uno, probabilmente per «accendere» la lampada associata.

Volendo indicare un gruppo di bit maggiore, basta aumentare il numero indicato a fianco dei nomi dei campi, come nell'esempio seguente:

struct Prova {
    unsigned char
        b0      :1,
        b1      :1,
        b2      :1,
        stato   :4;
};

Nell'esempio appena mostrato, si usano i primi tre bit in maniera singola (per qualche scopo) e altri quattro per contenere un'informazione «più grande». Ciò che resta (probabilmente solo un bit) viene semplicemente ignorato.

579.7   Istruzione «typedef»

L'istruzione typedef permette di definire un nuovo di tipo di dati, in modo che la sua dichiarazione sia più agevole. Lo scopo di tutto ciò sta nell'informare il compilatore; typedef non ha altri effetti. La sintassi del suo utilizzo è molto semplice:

typedef tipo nuovo_tipo;

Si osservi l'esempio seguente:

typedef int numero_t;
numero_t x, y, z;

In questo modo viene definito il nuovo tipo numero_t, corrispondente in pratica a un tipo intero, con il quale si dichiarano tre variabili: x, y e z. Le tre variabili sono di tipo numero_t. L'esempio seguente riguarda le enumerazioni:

typedef enum Colore { nero, marrone, rosso, arancio, giallo, 
                      verde, blu, viola, grigio, bianco } colore_t;
colore_t c, d;

In questo caso si definisce il tipo colore_t, corrispondente a un'enumerazione con i nomi dei colori principali. Le variabili c e d vengono dichiarate con questa modalità. Dal momento che si usa typedef, si potrebbe definire l'enumerazione in modo anonimo:

typedef enum { nero, marrone, rosso, arancio, giallo, 
               verde, blu, viola, grigio, bianco } colore_t;
colore_t c, d;

L'esempio successivo riguarda le strutture:

struct Datario {
    int giorno;
    int mese;
    int anno;
};
typedef struct Datario data_t;
data_t inizio, fine;

Attraverso typedef è stato definito il tipo data_t, facilitando così la dichiarazione delle variabili inizio e fine. Ma in questo caso, si presta di più una struttura anonima:

:-)

typedef struct {
    int giorno;
    int mese;
    int anno;
} data_t;
data_t inizio, fine;

Tradizionalmente, i nomi dei tipi di dati creati con l'istruzione typedef hanno estensione _t.

579.8   Costanti letterali composte

È possibile rappresentare un array o una struttura attraverso una costante letterale, nota come costante letterale composta. Formalmente si definisce la costante letterale composta secondo il modello seguente, dove le parentesi graffe fanno parte della definizione:

(tipo) { valore[, valore] }

Per comprenderne l'utilizzo servono degli esempi e il caso più semplice riguarda la definizione degli array:

int *p = (int []) {3, 5, 76};

In questo modo si dichiara un array di interi, contenente rispettivamente i valori 3, 5 e 76, il cui indirizzo iniziale viene assegnato al puntatore p. La variante seguente fa sì che il contenuto dell'array non possa essere modificato, ma per questo deve rendere altrettanto invariabile il contenuto raggiunto attraverso il puntatore:

const int *p = (const int []) {3, 5, 76};

Un array in forma letterale può essere trasmesso a una funzione. Quello che segue è un programma completo per dimostrare tale possibilità:

#include <stdio.h>

void f (int i[])
{
    printf ("i: %d %d %d\n", i[0], i[1], i[2]);
}

int main (int argc, char *argv[])
{
    f ((int []) {1, 3, 7});
    return 0;
}

In pratica, la funzione f() viene chiamata passando come argomento un array di tre interi, il quale logicamente viene trasmesso solo attraverso il puntatore al primo dei suoi elementi.

In modo analogo si possono rappresentare le strutture, ma in tal caso occorre disporre di un modello di riferimento, come si può vedere nell'esempio seguente che costituisce un altro programma completo:

#include <stdio.h>

struct Elenco {
    char  uno;
    short due;
    int   tre;
};

int main (int argc, char *argv[])
{
    struct Elenco e;
    e = (struct Elenco) { 33, 55, 77 };
    printf ("struttura: %d %d %d\n", e.uno, e.due, e.tre);
    return 0;
}

Ma naturalmente, i valori della struttura possono essere abbinati esplicitamente ai componenti a cui appartengono:

...
    e = (struct Elenco) { .uno=33, .tre=77, .due=55 };
...

Come per il caso degli array, anche le strutture rappresentate in forma letterale possono essere usate tra gli argomenti di una funzione. L'esempio seguente fa la stessa cosa di quello appena mostrato, con la differenza che si avvale di una funzione per ottenere lo scopo:

#include <stdio.h>

struct Elenco {
    char  uno;
    short due;
    int   tre;
};

void f (struct Elenco e)
{
    printf ("struttura: %d %d %d\n", e.uno, e.due, e.tre);
}

int main (int argc, char *argv[])
{
    f ((struct Elenco) { 33, 55, 77 });
    return 0;
}

Anche in questo caso, naturalmente, si possono rendere espliciti i componenti della struttura a cui si attribuiscono i valori:

...
    f ((struct Elenco) { .uno=33, .tre=77, .due=55 });
...

A differenza dell'array, la struttura che si trova tra gli argomenti di una funzione viene passata integralmente; volendo trasmettere solo il suo indirizzo, si può usare l'operatore &, come nell'esempio seguente:

#include <stdio.h>

struct Elenco {
    char  uno;
    short due;
    int   tre;
};

void f (struct Elenco *e)
{
    printf ("struttura: %d %d %d\n", e->uno, e->due, e->tre);
}

int main (int argc, char *argv[])
{
    f (&(struct Elenco) {.uno=33, .tre=77, .due=55});
    return 0;
}

Appunti di informatica libera 2008 --- Copyright © 2000-2008 Daniele Giacomini -- <appunti2 (ad) gmail·com>


Dovrebbe essere possibile fare riferimento a questa pagina anche con il nome c_struttura_unione_campo_enumerazione_costante_composta.htm

[successivo] [precedente] [inizio] [fine] [indice generale] [indice ridotto] [indice analitico] [home]

Valid ISO-HTML!

CSS validator!

Gjlg Metamotore e Web Directory