Barninga Z
a- a+

Le strutture

ENTITA' COMPLESSE 

Le strutture 

Tra gli strumenti cui si è fatto cenno appare fondamentale la struttura (structure), mediante la quale si definisce un modello (template) che individua un'aggregazione di tipi di dato fondamentali. 

Ecco come potremmo descrivere un pixel con l'aiuto di una struttura: 

struct pixel {
    int x;
    int y;
    int colour;
};

Quello dell'esempio è una dichiarazione di template di struttura: si apre con la parola chiave struct seguita dal nome (tag) che intendiamo dare al nostro modello; questo è a sua volta seguito da una graffa aperta. Le righe che seguono, vere e proprie dichiarazioni di variabili, individuano il contenuto della struttura e si concludono con una graffa chiusa seguita dal punto e virgola. 

Ecco un altro esempio di dichiarazione di template, dal quale risulta chiaro che una struttura può comprendere differenti tipi di dato: 

struct ContoCorrente {
    char   intestatario[50];
    char   data_accensione[9];
    int    cod_filiale;
    double saldo;
    double tasso_interesse;
    double max_fido;
    double tasso_scoperto;
};

E' meglio focalizzare sin d'ora che la dichiarazione di un template di struttura non comporta che il compilatore riservi dello spazio di memoria per  allocare i campi[36] della struttura stessa. La dichiarazione di template definisce semplicemente la "forma" della struttura, cioè il suo modello. 

Di solito le dichiarazioni di template di struttura compaiono all'inizio del sorgente, anche perché i templates devono essere stati dichiarati per poter essere utilizzati: solo dopo avere definito l'identifictore (tag) e il modello (template) della struttura, come negli esempi di poco fa, è possibile dichiarare ed utilizzare oggetti di quel tipo, vere e proprie variabili struct

 

#include

struct concorso {
    int serie;
    char organizzatore;
    int partecipanti;
};

void main(void)
{
    struct concorso c0, c1;

    c0.serie = 2;
    c0.organizzatore = 'F';
    c0.partecipanti = 482;
    c1.serie = 0;
    c1.organizzatore = 'G';
    c1.partecipanti = 33;
    printf("Serie della concorso 0: %d
" ,c0.serie);
    printf("Organizzatore della concorso 1: %c
" ,c1.organizzatore);
}

Nel programma dell'esempio viene dichiarato un template di struttura, avente tag concorso. Il template è poi utilizzato in main() per dichiarare due strutture di tipo concorso: solo a questo punto sono creati gli oggetti concorso e viene loro riservata memoria. Gli elementi, o campi, delle due strutture sono inizializzati con dati di tipo opportuno; infine alcuni di essi sono visualizzati con la solita printf()

Cerchiamo di evidenziare alcuni concetti fondamentali, a scanso di equivoci. La dichiarazione di template non presenta nulla di nuovo: parola chiave struct, tag, graffa aperta, campi, graffa chiusa, punto e virgola. Una novità è invece rappresentata dalla dichiarazione delle strutture c0 e c1: come si vede essa è fortemente analoga a quelle di comuni variabili, con la differenza che le variabili dichiarate non appartengono al tipo int, float, o a uno degli altri tipi di dati sin qui trattati. Esse appartengono ad un tipo di dato nuovo, definito da noi: il tipo struct concorso

Finora si è indicato con "dichiarazione di template" l'operazione che serve a definire l'aspetto della struttura, e con "dichiarazione di struttura" la creazione degli oggetti, cioè la dichiarazione delle variabili struttura. E' forse una terminologia prolissa, ma era indispensabile per chiarezza. Ora che siamo tutti diventati esperti di strutture potremo essere un poco più concisi e indicare con il termine "struttura" , come comunemente avviene, tanto i template che le  variabili di tipo struct[37]. 

In effetti, la dichiarazione: 

struct concorso {
    int serie;
    char organizzatore;
    int partecipanti;
};

crea semplicemente un modello che può essere usato come riferimento per ottenere variabili dotate di quelle particolari caratteristiche. Ciascuna variabile conforme a quel modello contiene, nell'ordine prefissato, un int, un char e un secondo int. A ciascuna di queste variabili, come per quelle di qualsiasi altro tipo, il compilatore alloca un'area di memoria di dimensioni sufficienti, alla quale associa il nome simbolico che compare nella dichiarazione 

struct concorso c0;

cioè c0. In quest'ultima dichiarazione, l'identificatore concorso indica il modello particolare al quale si deve conformare la variabile dichiarata. Esso è, in pratica, un'abbreviazione di 

{
    int serie;
    char organizzatore;
    int partecipanti;
};

e come tale può venire usato nel programma. In altre parole, è possibile riferirsi all'intera dichiarazione di struttura semplicemente usandone il tag. 

Una variabile di tipo struct può essere dichiarata contestualmente al template: 

struct concorso {
    int serie;
    char organizzatore;
    int partecipanti;
} c0, c1;

Il template può essere normalmente utilizzato per dichiarare altre  strutture nel programma[38]. 

Tornando a quel che avviene nella main() dell'esempio, ai campi delle strutture dichiarate sono stati assegnati valori con una notazione del tipo 

nome_della_variabile_struttura.nome_del_campo = valore;

ed in effetti l'operatore punto (".") è lo strumento offerto dal C per accedere ai singoli campi delle variabili struct, tanto per assegnarvi un valore, quanto per leggerlo (e lo si vede dalle printf() che seguono). 

Abbiamo parlato delle strutture viste negli esempi precedenti come di variabili di tipo struct concorso. In effetti definire un template di struttura significa arricchire il linguaggio di un nuovo tipo di dato, non intrinseco, ma al quale è possibile applicare la maggior parte dei concetti e degli strumenti disponibili con riferimento ai tipi di dato intrinseci. 

Le strutture possono quindi essere gestite mediante array e puntatori, proprio come comuni variabili C. La dichiarazione di un array di strutture si prsenta come segue: 

struct concorso c[3];

Si nota immediatamente la forte somiglianza con la dichiarazione di un array di tipo intrinseco: il valore tra parentesi quadre specifica il numero di elementi, cioè, in questo caso, di strutture che formano l'array. Ogni elemento è, appunto, una struttura conforme al template concorso; l'array ha nome c. Per accedere ai singoli elementi dell'array è necessario, come prevedibile, specificare il nome dell'array seguito dall'indice, tra quadre, dell'elemento da referenziare. La differenza rispetto ad un array "comune" , ad esempio di tipo int, sta nel fatto che accedere ad una struttura non significa ancora accedere ai dati che essa contiene: per farlo occorre usare l'operatore punto, come mostrato poco sopra. Un esempio chiarirà le idee: 

#include

struct concorso {
    int serie;
    char organizzatore;
    int partecipanti;
};

void main(void)
{
    register i;
    struct concorso c[3];

    c[0].serie = 2;
    c[0].organizzatore = 'F';
    c[0].partecipanti = 482;
    c[1].serie = 0;
    c[1].organizzatore = 'G';
    c[1].partecipanti = 33;
    c[2].serie = 3;
    c[2].organizzatore = 'E';
    c[2].partecipanti = 107;
    for(i = 0; i < 3; i++)
        printf("%d    %c    %d
" ,c[i].serie,c[i].organizzatore,c[i].partecipanti);
}

 

Con riferimento ad un array di strutture, la sintassi usata per referenziare i campi di ciascuna struttura elemento dell'array è simile a quella utilizzata per array di tipi intrinseci. Ci si riferisce, ad esempio, al campo serie dell'elemento di posto 0 dell'array con la notazione c[0].serie; è banale osservare che c[0] accede all'elemento dell'array, mentre .serie accede al campo voluto di quell'elemento. 

Si può pensare all'esempio presentato sopra immaginando di avere tre fogli di carta, ciascuno contenente un elemento dell'array c. In ciascun foglio sono presenti tre righe di informazioni che rappresentano, rispettivamente, i 3 campi della struttura. Se i 3 fogli vengono mantenuti impilati in ordine numerico crescente, si ottiene una rappresentazione "concreta" dell'array, in quanto è possibile conoscere sia il contenuto dei tre campi di ogni elemento, sia la relazione tra i vari elementi dell'array stesso. 

I più attenti hanno  sicuramente[39] notato che, mentre le operazioni di assegnamento, lettura, etc. con tipi di dato intrinseci vengono effettuate direttamente sulla variabile dichiarata, nel caso delle strutture esse sono effettuate sui campi, e non sulla struttura come entità direttamente accessibile. In realtà le regole del C non vietano di accedere direttamente ad una struttura intesa come un'unica entità, ma si tratta di una  pratica poco seguita[40]. E' infatti assai più comodo ed efficiente utilizzare i puntatori. 

Anche nel caso dei puntatori le analogie tra strutture e tipi intrinseci sono forti. La dichiarazione di un puntatore a struttura, infatti, è: 

struct concorso *cPtr;

dove cPtr è il puntatore, che può contenere l'indirizzo di una struttura di template concorso. L'espressione *cPtr restituisce una struct concorso, esattamente come in una dichiarazione quale 

int *iPtr;

*iPtr restituisce un int. Attenzione, però: per accedere ai campi di una struttura referenziata mediante un puntatore non si deve usare l'operatore punto, bensì l'operatore "freccia" , formato dai caratteri "meno" ("­") e "maggiore" (">") in sequenza, con una sintassi del tipo: 

nome_del_puntatore_alla_variabile_di_tipo_struttura->
nome_del_campo = valore;

Vediamo un esempio. 

 

struct concorso *cPtr;

    ....
    cPtr->serie = 2;
    ....
    printf("Serie: %d
" ,cPtr->serie);

I puntatori a struttura godono di tutte le proprietà dei puntatori a tipi intrinseci, tra le quali particolarmente interessante appare l'aritmetica dei puntatori. Incrementare un puntatore a struttura significa sommare implicitamente al suo valore tante unità quante ne occorrono per "scavalcare" tutta la struttura referenziata e puntare quindi alla successiva. In generale, sommare un intero ad un puntatore a struttura equivale sommare quell'intero moltiplicato per la  dimensione della struttura[41]. E' appena il caso di sottolineare che la dimensione di un puntatore a struttura e la dimensione della struttura puntata sono due concetti differenti, come già si è detto per le variabili di tipo intrinseco. Un puntatore a struttura occupa sempre 2 o 4 byte, a seconda che sia near, oppure far o huge, indipendentemente dalla dimensione della struttura a cui punta. Con la dichiarazione di un puntatore a struttura, dunque, il compilatore non alloca memoria per la struttura stessa. 

Rivediamo il programma d'esempio di poco fa, modificandolo per utilizzare un puntatore a struttura: 

 

#include

struct concorso {
    int serie;
    char organizzatore;
    int partecipanti;
};

void main(void)
{
    struct concorso c[3], *cPtr;

    c[0].serie = 2;
    c[0].organizzatore = 'F';
    c[0].partecipanti = 482;
    c[1].serie = 0;
    c[1].organizzatore = 'G';
    c[1].partecipanti = 33;
    c[2].serie = 3;
    c[2].organizzatore = 'E';
    c[2].partecipanti = 107;
    for(cPtr = c; cPtr < c+3; ++cPtr)
        printf("%d   %c   %d
" ,cPtr->serie,cPtr->organizzatore,cPtr->partecipanti);
}

Come si può notare, la modifica consiste essenzialmente nell'avere dichiarato un puntatore a struct concorso, cPtr, e nell'averlo utilizzato in luogo della notazione c[i] per accedere agli elementi dell'array. Le dichiarazioni dell'array e del puntatore sono state raggruppate in un'unica istruzione, ma sarebbe stato possibile separarle: il codice 

struct concorso c[3];
    struct concorso *cPtr;

avrebbe avuto esattamente lo stesso significato, sebbene in forma meno compatta e, forse, più leggibile. 

Nel ciclo for dell'esempio, il puntatore cPtr è inizializzato a c e poiché il nome di un array è puntatore all'array stesso, cPtr punta al primo elemento di c, cioè c[0]. Durante la prima iterazione sono visualizzati i valori dei 3 campi di c[0]; all'iterazione successiva cPtr viene incrementato per puntare al successivo elemento di c, cioè c[1], e quindi l'espressione 

cPtr->

è ora equivalente a 

c[1].

All'iterazione successiva, l'espressione 

cPtr->

diviene equivalente a 

c[2].

dal momento che cPtr è stato incrementato ancora una volta. 

A proposito di puntatori, è forse il caso di evidenziare che una struttura può contare tra i suoi campi puntatori a qualsiasi tipo di dato. Sono perciò ammessi anche puntatori a struttura, persino puntatori a struttura identificata dal medesimo tag. In altre parole, è perfettamente lecito scrivere: 

struct TextLine {
    char *line;
    int  cCount;
    struct TextLine *prevTL;
    struct TextLine *nextTL;
};

Quella dell'esempio è una struttura (o meglio, un template di struttura) che potrebbe essere utilizzata per una rudimentale gestione delle righe di un testo, ad esempio in un programma di word processing. Essa contiene due puntatori a struttura dello stesso template: nell'ipotesi che ogni riga di testo sia gestita attraverso una struttura TextLine, prevTL è valorizzato con l'indirizzo della struct TextLine relativa alla riga precedente nel testo, mentre nextTL punta alla struct TextLine della  riga successiva[42]. E' proprio mediante un utilizzo analogo a questo dei puntatori che vengono implementati oggetti quali le liste. Uno dei vantaggi immediatamente visibili che derivano dall'uso descritto dei due puntatori prevTL e nextTL consiste nella possibilità di implementare algoritmi di ordinamento delle righe di testo che agiscano solo sui puntatori: è sufficiente modificare il modo in cui le righe di testo sono legate l'una all'altra da un punto di vista logico, senza necessità alcuna di modificarne l'ordine fisico in memoria. 

E' ovvio che, come al solito, un puntatore non riserva lo spazio per l'oggetto a cui punta. Nell'ipotesi di puntatori near, l'espressione sizeof(struct TextLine) restituisce 8. La memoria necessaria a contenere la riga di testo e le strutture TextLine stesse deve essere allocata esplicitamente. 

Nel caso degli array, al contrario, la memoria è allocata staticamente dal compilatore (anche qui nulla di nuovo): riscriviamo il template in modo da gestire la riga di testo come un array di caratteri, avente dimensione massima prestabilita (in questo caso 80): 

struct TextLine {
    char line[80];
    int  cCount;
    struct TextLine *prevTL;
    struct TextLine *nextTL;
};

questa volta l'espressione sizeof(struct TextLine) restituisce 86. 

Va anche precisato che una struttura può contenere un'altra struttura (e non solo il puntatore ad essa), purché identificata da un diverso tag: 

struct TextParms {
    int textLen;
    int indent;
    int justifyType;
};

struct TextLine {
    char line[80];
    struct TextParms lineParms;
    struct TextLine *prevTL;
    struct TextLine *nextTL;
};

In casi come questo le dichiarazioni delle due strutture possono perfino essere nidificate: 

 

struct TextLine {
    char line[80];
    struct TextParms {
        int textLen;
        int indent;
        int justifyType;
    } lineParms;
    struct TextLine *prevTL;
    struct TextLine *nextTL;
};

Da quanto appena detto appare evidente che una struttura non può mai contenere una struttura avente il proprio stesso tag identificativo: per il compilatore sarebbe impossibile risolvere completamente la definizione della struttura, in quanto essa risulterebbe definita in funzione di se stessa. In altre parole è illecita una dichiarazione come: 

 

struct ST {
    int number;    // OK
    float *fPtr;   // OK
    struct ST inner;       // NO! il tag e' il medesimo e questo non e' un puntatore
};

 

Anche agli elementi di strutture nidificate si accede tramite il punto (".") o la freccia ("­>"): con riferimento ai templates appena riportati, è possibile, ad esempio, dichiarare un array di strutture TextLine

struct TextLine tl[100];       // dichiara un array di strutture TextLine

Alla riga di testo gestita dal primo elemento dell'array si accede, come già sappiamo, con l'espressione tl[0].line. Per visualizzare la riga successiva (gestita dall'elemento di tl il cui indirizzo è contenuto in nextTL) vale la seguente: 

printf("Prossima riga: %s " ,tl[0].nextTL->line);

Infatti tl[0].nextTL accede al campo nextTL di tl[0]: l'operatore utilizzato è il punto, proprio perchè tl[0] è una struttura e non un puntatore a struttura. Ma nextTL è, al contrario, un puntatore, perciò per referenziare l'elemento line della struttura che si trova all'indirizzo che esso contiene è necessario usare la "freccia". Supponiamo ora di voler conoscere l'indentazione (rientro rispetto al margine) della riga appena visualizzata: è ormai noto che ai campi della struttura "puntata" da nextTL si accede con l'operatore ­>; se il campo referenziato è, a sua volta, una struttura (lineParms), i campi di questa sono "raggiungibili" mediante il punto. 

printf("Indentazione della prossima riga: %d " ,tl[0].nextTL->lineParms.indent);

Insomma, la regola generale (che richiede di utilizzare il punto se l'elemento fa parte di una struttura referenziata direttamente e la freccia se l'elemento è raggiungibile attraverso il puntatore alla struttura) rimane valida e si applica pedestremente ad ogni livello di nidificazione. 

Vale infine la pena di chiarire che le strutture, pur costituendo un potente strumento per la rappresentazione informatica di entità complesse (quali record di archivi, etc.), sono ottimi "aiutanti" anche quando si desideri semplificare il codice ed incrementarne l'efficienza; se, ad esempio, occorre passare molti parametri ad una funzione, e questa è richiamata molte volte (si pensi al caso di un ciclo con molte iterazioni), può essere conveniente definire una struttura che raggruppi tutti quei parametri, così da poter passare alla funzione un parametro soltanto: il puntatore alla struttura stessa.



Ti potrebbe interessare anche

commenta la notizia

C'è 1 commento
Staff
Ti interessano altri articoli su questo argomento?
Chiedi alla nostra Redazione!