|
Appunti informatica |
|
Visite: 2683 | Gradito: | [ Medio appunti ] |
Leggi anche appunti:Impiego del costrutto monitorImpiego del costrutto monitor XII) Scrivere una applicazione I device driverI device driver Sempre più difficile: dopo avere affrontato i TSR (pag. 169) Gli interrupt: utilizzoGli interrupt: utilizzo Gli interrupt sono routine, normalmente operanti a livello |
I tipi di dato discussi in precedenza sono intrinseci al compilatore: quelli, cioè, che esso è in grado di gestire senza ulteriori costruzioni logiche da parte del programmatore; possiamo indicarli come tipi elementari.
Spesso, però, essi non sono sufficienti a rappresentare in modo esauriente le realtà oggetto di elaborazione: In un semplice programma che gestisca in modo grafico il monitor del computer può essere comodo rappresentare un generico punto luminoso (pixel) del monitor stesso come un'entità unica, individuata mediante parametri che consentano, attraverso il loro valore, di distinguerla dalle altre dello stesso tipo: si tratta di un'entità complessa.
Infatti ogni pixel può essere descritto, semplificando un po', mediante tre parametri caratteristici: le coordinate (che sono due, ascissa e ordinata, trattandosi di uno spazio bidimensionale) e il colore.
Il C mette a disposizione del programmatore alcuni strumenti atti a rappresentare entità complesse in modo più prossimo alla percezione che l'uomo ne ha, di quanto consentano i tipi di dato finora visti. Non si tratta ancora della possibilità di definire veri e propri tipi di dato 'nuovi' e di gestirli come se fossero intrinseci al linguaggio , ma è comunque un passo avanti
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 ;
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 ;
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 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 <stdio.h>
struct concorso ;
void main(void)
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
In effetti, la dichiarazione:
struct concorso ;
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
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 c0, c1;
Il template può essere normalmente utilizzato per dichiarare altre strutture nel programma
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 <stdio.h>
struct concorso ;
void main(void)
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 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 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 . 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: %dn',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 (vedere pag. ). 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 . 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 <stdio.h>
struct concorso ;
void main(void)
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 ;
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 . 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 . 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
struct TextLine ;
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 ;
struct TextLine ;
In casi come questo le dichiarazioni delle due strutture possono perfino essere nidificate:
struct TextLine 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 ;
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: %sn',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: %dn',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 (vedere pag.
Da quanto detto circa le strutture, appare evidente come esse costituiscano uno strumento per la rappresentazione di realtà complesse, in quanto sono in grado di raggrupparne i molteplici aspetti quantitativi . In particolare, ogni singolo campo di una struttura permette di gestire uno degli aspetti che, insieme, descrivono l'oggetto reale.
Il concetto di unione deriva direttamente da quello di struttura, ma con una importante differenza: i campi di una union rappresentano diversi modi di vedere, o meglio, rappresentare, l'oggetto che la union stessa descrive. Consideriamo l'esempio seguente:
struct FarPtrWords ;
union Far_c_Ptr ;
La dichiarazione di un template di union è del tutto analoga a quella di un template di struttura: l'unica differenza è costituita dalla presenza della parola chiave union in luogo di struct
La differenza è però enorme a livello concettuale: la struct FarPtrWords comprende due campi, entrambi di tipo unsigned int. Non ci vuole molto a capire che essa occupa 4 byte e descrive un puntatore di tipo 'non near', scomposto nelle due componenti di indirizzo
I due campi della union Far_c_Ptr, invece, sono rispettivamente un puntatore a 32 bit e una struct FarPtrWords. Contrariamente a quanto ci si potrebbe aspettare, la union non occupa 8 byte, bensì solo 4: puntatore e struct FarPtrWords sono due modi alternativi di interpretarli o, in altre parole, di accedere al loro contenuto. La union Far_c_Ptr è un comodo strumento per gestire un puntatore come tale, o nelle sue parti offset e segmento, a seconda della necessità. L'area di memoria in cui il dato si trova è sempre la stessa, ma il campo ptr la referenzia come un tutt'uno, mentre la struttura FarPtrWord consente di accedere ai primi due byte o agli ultimi due, separatamente.
Si può pensare ad una union come ad un insieme di 'maschere' attraverso le quali interpretare il contenuto di un'area di memoria.
Vediamo la sintassi, senza preoccuparci dell'espressione (char far *): si tratta di un cast (pag. ) e non riguarda in modo diretto l'argomento 'union
union FarPtr fp;
fp.ptr = (char far *)0xB8000000L;
printf('ptr: %Fpn',fp.ptr);
printf('ptr: %X:%Xn',fp.words.segment,fp.words.offset)
L'accesso ai membri di una union segue le medesime regole dell'accesso ai membri di una struct, cioè mediante l'operatore punto (o l'operatore 'freccia' se si lavora con un puntatore). E' interessante notare che inizializzando il campo ptr viene inizializzato anche il campo word, in quanto condividono la stessa memoria fisica. Il medesimo campo ptr è poi utilizzato per ricavare il valore del puntatore, mentre il campo words consente di accedere alle componenti segment ed offset. Entrambe le printf() visualizzano
B800:0000
Se nel codice dell'esempio si sostituisce la riga di inizializzazione del campo ptr con le righe seguenti:
fp.words.offset = 0;
fp.words.segment = 0xB800;
le due printf() visualizzano ancora il medesimo output.
La sintassi che consente di accedere ai due campi della struct FarPtrWords, a prima vista, può apparire strana, ma in realtà essa è perfettamente coerente con le regole esposte con riferimento alle strutture: dal momento che ai campi di una union si accede mediante l'operatore punto, sono giustificate le scritture fp.ptr e fp.words ma l'operatore punto si utilizza anche per accedere ai membri di una struttura, perciò sono lecite le scritture word.offset e word.segment; ciò spiega fp.word.offset e fp.word.segment
Nell'esempio di union analizzato, i membri sono due ed hanno uguale dimensione (entrambi 4 byte). Va precisato che i membri di una union possono essere più di due; inoltre essi possono essere di dimensioni differenti l'uno dall'altro, nel qual caso il compilatore, allocando la union, le riserva una quantità di memoria sufficiente a contenere il più 'ingombrante' dei suoi membri, e li 'sovrappone' a partire dall'inizio dell'area di memoria occupata dalla union stessa. Esempio:
union Far_c_Ptr
Il terzo elemento della union è un unsigned int, e come tale occupa 2 byte. Questi coincidono con i primi due byte di ptr (e della struct), e rappresentano pertanto la word offset del puntatore rappresentato dalla union
I puntatori a union si comportano esattamente come i puntatori a struct
Gli enumeratori sono un ulteriore strumento che il C rende disponibile per rappresentare più agevolmente i dati gestiti dai programmi. In particolare essi consentono di descrivere con nomi simbolici gruppi di oggetti ai quali è possibile associare valori numerici interi.
Come noto, le variabili di un programma possono rappresentare non solo oggetti quantificabili, come un importo valutario, ma anche qualità non numerabili (come un colore o il sesso di un individuo) la cui caratteristica principale è il fatto di essere mutuamente esclusive. Normalmente si tende a gestire tali qualità 'inventando' una codifica che permette di assegnare valori di tipo integral (gli smemorati tornino a pag. ) ai loro differenti modi di manifestarsi (ad esempio: al colore nero può essere associato il valore zero, al rosso il valore , e così via; si può utilizzare il carattere 'M' per 'maschile' e 'F' per 'femminile, etc.). Spesso si ricorre alle direttive #define, che consentono di associare, mediante la sostituzione di stringhe a livello di preprocessore, un valore numerico ad un nome descrittivo (vedere pag. e seguenti).
L'uso degli enumeratori può facilitare la stesura dei programmi, lasciando al compilatore il compito di effettuare la codifica dei diversi valori assumibili dalle variabili che gestiscono modalità qualitative, e consentendo al programmatore di definire ed utilizzare nomi simbolici per riferirsi a tali valori. Vediamo un esempio:
enum SEX ;
La dichiarazione di un enumeratore ricorda da vicino quella di una struttura: anche in questo caso viene definito un template; la parola chiave enum è seguita dal tag, cioè dal nome che si intende dare al modello di enumeratore; vi sono le parentesi graffe aperta e chiusa, quest'ultima seguita dal punto e virgola. La differenza più evidente rispetto alla dichiarazione di un template di struttura consiste nel fatto che laddove in questo compaiono le dichiarazioni dei campi (vere e proprie definizioni di variabili con tanto di indicatore di tipo e punto e virgola), nel template di enum vi è l'elenco dei nomi simbolici corrispondenti alle possibili manifestazioni della qualità che l'enumeratore stesso rappresenta. Detti nomi simbolici sono separati da virgole; la virgola non compare dopo l'ultimo nome elencato.
Anche la dichiarazione di una variabile di tipo enum ricorda da vicino quella di una variabile struttura:
enum SEX sesso;
sesso = maschile;
.
if(sesso == maschile)
printf('MASCHIO');
else
if(sesso == femminile)
printf('FEMMINA');
else
printf('BOH?');
Il codice riportato chiarisce le modalità di dichiarazione, inizializzazione e, in generale, di utilizzo di una variabile di tipo enum
E' inoltre possibile notare come in C, a differenza di quanto avviene in molti altri linguaggi, l'operatore di assegnamento e quello di confronto per uguaglianza hanno grafia differente, dal momento che quest'ultimo si esprime con il doppio segno di uguale.
Ovviamente il compilatore, di soppiatto, assegna dei valori ai nomi simbolici elencati nel template dell'enum: per default al primo nome è associato il valore , al secondo , e così via. E' comunque possibile assegnare valori a piacere, purché integral, ad uno o più nomi simbolici; ai restanti il valore viene assegnato automaticamente dal compilatore, incrementando di uno il valore associato al nome precedente.
enum SEX ;
Nell'esempio, al nome ignoto è assegnato esplicitamente valore : il compilatore assegna valore al nome maschile e a femminile. I valori esplicitamente assegnati dal programmatore non devono necessariamente essere consecutivi; la sola condizione da rispettare è che si tratti di valori interi.
Il vantaggio dell'uso degli enumeratori consiste nella semplicità di stesura e nella migliore leggibilità del programma, che non deve più contenere dichiarazioni di costanti manifeste né utilizzare variabili intere per esprimere modalità qualitative. Inoltre, la limitazione del fabbisogno di costanti manifeste rappresenta di per sé un vantaggio di carattere tecnico, in quanto consente di limitare i rischi connessi al loro utilizzo, in particolare i cosiddetti side effect o effetti collaterali (pag.
Se una variabile di tipo intero può assumere solo un limitato numero di valori, è teoricamente possibile memorizzarla utilizzando un numero di bit inferiore a quello assegnatole dal compilatore: basta infatti 1 bit per memorizzare un dato che può assumere solo due valori, 2 bit per un dato che può assumere quattro valori, 3 bit per uno che può assumere otto valori, e così via.
Il C non ha tipi intrinseci di dati con un numero di bit inferiori a 8 (il char), ma consente di 'impaccare' più variabili nel numero di bit strettamente necessario mediante i cosiddetti campi di bit.
Un esempio di uso di questo strumento può essere ricavato con riferimento alla gestione di una cartella clinica. Supponiamo di voler gestire, per ogni paziente, le seguenti informazioni: il sesso (maschile o femminile), lo stato vitale (vivente, defunto, in coma), il tipo di medicinale somministrato (sedici categorie, come antibiotici e sulfamidici), la categoria di ricovero (otto possibili sistemazioni, da corsia a camera di lusso). In questa ipotesi sarebbe possibile codificare il sesso mediante un solo bit, lo stato vitale con 2, il tipo di cura con 4, la sistemazione in ospedale con 3: in totale 10 bit, senz'altro disponibili in un'unica variabile di tipo intero.
L'uso dei campi di bit prevede la dichiarazione di un template: anche in questo caso la somiglianza con le strutture è palese.
struct CartellaClinica ;
La dichiarazione utilizza la parola chiave struct, proprio come se si trattasse di un template di struttura; le dichiarazioni dei campi sono introdotte da uno specificatore di tipo e chiusa dal punto e virgola; la differenza qui consiste nell'indicazione dell'ampiezza in bit di ogni singolo campo, effettuata posponendo al nome del campo il carattere due punti (' ') seguito dal numero di bit da assegnare al campo stesso. I due punti servono, infatti, a indicare la definizione di un campo di bit, la cui ampiezza viene specificata dal numero seguente; se il numero totale di bit non è disponibile in un'unica variabile intera, il compilatore alloca anche la successiva word in memoria.
I campi di bit del tipo CartellaClinica sono tutti dichiarati unsigned int: in tal modo tutti i bit sono utilizzabili per esprimere i valori che di volta in volta i campi stessi assumeranno. In realtà, i campi di bit possono anche essere dichiarati int, ma in questo caso il loro bit più significativo rappresenta il segno e non è quindi disponibile per memorizzare il valore. Un campo dichiarato int ed ampio un solo bit può esprimere solo i valori e
I campi di bit sono referenziabili esattamente come i campi di una comune struttura
enum SEX ;
struct CartellaClinica ;
char *sessi[] = ;
struct CartellaClinica Paziente;
.
Paziente.sesso = maschile;
.
printf('Sesso del paziente: %sn',sessi[Paziente.sesso]);
E' importante ricordare come sia compito del programmatore assicurarsi che i valori memorizzati nei campi di bit non occupino più bit di quanti ne sono stati loro riservati in fase di definizione del template, dal momento che le regole del C non assicurano che venga effettuato un controllo nelle operazioni di assegnamento di valori ai campi. Se si assegna ad un campo di bit un valore maggiore del massimo previsto per quel campo, può accadere che i bit più significativi di quel valore siano scritti nei campi successivi: è bene, ancora una volta, verificare il comportamento del proprio compilatore (circa le dipendenze del codice dal compilatore utilizzato, vedere pag.
Naturalmente un campo di bit può essere utilizzato anche per memorizzare un'informazione di tipo quantitativo: ad esempio, la struct CartellaClinica potrebbe essere ridefinita mediante l'aggiunta di un campo atto a memorizzare il numero di ricoveri subiti dal paziente; impiegando 6 bit tale valore è limitato a 63.
struct CartellaClinica ;
Nella nuova definizione, tutti i 16 bit delle due word occupate in memoria dalla struct CartellaClinica sono utilizzati.
In tal senso strumenti molto potenti sono offerti dal C++, con il quale si possono 'inventare' nuovi tipi di dato, definendone anche le modalità di manipolazione, e gestirli, se ben progettati, senza alcuna differenza rispetto a quelli elementari intrinseci.
Nella pratica comune ci si riferisce di solito alla 'struttura concorso' allo stesso modo che alla 'struttura c0', anche se nel primo caso si intende 'il modello della struttura il cui identificatore è concorso' e nel secondo 'la variabile di nome c0, il cui tipo è la struct avente modello concorso'. I distratti sono avvertiti.
Per completezza va osservato che dichiarando la variabile struttura e contemporaneamente definendone il template la creazione di un tag può essere omessa:
struct c0, c1;
In questo caso, non esistendo un tag mediante il quale fare riferimento al template, è necessario riscrivere il template ogni volta che si dichiara altrove una variabile avente quelle stesse caratteristiche. Da evitare, assolutamente.
Forse anche perché fino a qualche anno fa erano pochi i compilatori in grado di implementare tale sintassi.
La dimensione di una struttura può essere ricavata mediante l'operatore sizeof() (vedere pag. ), passandogli quale argomento il tag preceduto dalla parola chiave struct, oppure il nome di una variabile struttura: basandoci sugli esempi visti sin qui, sizeof(struct concorso) e sizeof(c0) restituiscono entrambe la dimensione della struttura concorso (che nel nostro caso è pari a 5 byte).
Un prevTL e un nextTL contenenti NULL segnalano che le righe gestite dalle strutture di cui fanno parte sono, rispettivamente, la prima e l'ultima del testo. Se una struct TextLine presenta entrambi i puntatori NULL, allora essa gestisce l'unica riga di testo. Se è NULL anche il puntatore alla riga, line, allora il testo è vuoto.
Perché prima l'offset e poi il segmento? E' sempre la solita storia: i processori Intel memorizzano i byte più significativi di una variabile nelle locazioni di memoria aventi indirizzo maggiore (tecnica backwords). Perciò di un dato a 32 bit è memorizzato prima il quarto byte, poi il terzo (ed è la seconda word), poi il secondo, infine il primo (ed è la prima word).
Appunti su: |
|