- Programmazione » Programmazione » Guida C - Manuale programmazione con articoli e risorse interessanti
Le strutture
ENTITA' COMPLESSE
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
Chiedi alla nostra Redazione!