La gestione dei dati in C 

Per poter parlare di come si gestiscono i dati, occorre prima precisare che cosa essi siano, o meglio che cosa si intenda con il termine "dati", non tanto dal punto di vista della logica informatica, quanto piuttosto da quello strettamente tecnico ed operativo. 

In tal senso, va innanzitutto osservato che tutto quanto viene elaborato dal microprocessore di un computer deve risiedere nella memoria di questo, la cosiddetta  RAM[1], che, al di là della sua implementazione hardware, è una sequenza di bit, ciascuno dei quali, ovviamente, può assumere valore 1 oppure 0. Nella RAM si trova anche il codice macchina eseguibile del programma: semplificando un poco, possiamo dire che tutta la parte di RAM non occupata da quello può rappresentare "dati". 

E' evidente che nella maggior parte dei casi un programma non controlla tutta la memoria, ma solo una porzione più o meno ampia di essa; inoltre le regole in base alle quali esso ne effettua la gestione sono codificate all'interno del programma stesso e dipendono, almeno in parte, dal linguaggio utilizzato per scriverlo. 

Sintetizzando quanto affermato sin qui, i dati gestiti da un programma sono sequenze di bit situate nella parte di RAM che esso controlla: se il programma vi può accedere in lettura e scrittura, dette sequenze rappresentano le cosiddette "variabili"; se l'accesso può avvenire in sola lettura si parla, invece, di "costanti". 

Dal punto di vista del loro significato si apre invece il discorso dei tipi di dato. 

I tipi di dato 

Al fine di attribuire significato ad una sequenza di bit occorre sapere quanti bit la compongono, e, come vedremo, qual è la loro organizzazione al suo interno. La più ristretta sequenza di bit significativa per le macchine è il byte, che si compone di 8 bit[2]

In C, al byte corrisponde il tipo di dato character, cioè carattere. Esso può assumere 256 valori diversi (28 = 256). Si distinguono due tipi di character: il signed character, in cui l'ottavo bit funge da indicatore di segno (se è 1 il valore è negativo), e l'unsigned character, che utilizza invece tutti gli 8 bit per esprimere il valore, e può dunque esclusivamente assumere valori positivi. Un signed char può variare tra ­128 e 127, mentre un unsigned char può esprimere valori tra 0 e 255

La sequenza di bit di ampiezza immediatamente superiore al byte è detta word. Qui il discorso si complica leggermente, perché mentre il byte si compone di 8 bit su quasi tutte le macchine, la dimensione della word dipende dal microprocessore che questa utilizza, e può essere, generalmente, di16 o32 bit. Nelle pagine che seguono faremo riferimento alla word come ad una sequenza di 16 bit, in quanto è tale la sua dimensione su tutte le macchine che utilizzano i processori Intel 8086 o 8088, o i chips 80286, 80386 e 80486 in modalità reale (cioè compatibile con l'Intel 8086). 

Il tipo di dato C corrispondente alla word è l'integer, cioè intero. Anche l'integer può essere signed o unsigned. Dando per scontato, come appena detto, che un integer (cioè una word) occupi 16 bit, i valori estremi del signed integer sono ­32768 e 32767, mentre quelli dell'unsigned integer sono 0 e 65535

Tra il character e l'integer si colloca lo short integer, che può essere, manco a dirlo, signed o unsigned. Lo short integer occupa 16 bit, perciò stanti le assunzioni sulla dimensione della word, ai nostri fini short integer e integer sono equivalenti. 

Per esprimere valori interi di notevole entità il C definisce il long integer, che occupa 32 bit. Anche il long integer può essere signed o unsigned. Nelle macchine in cui la word è di 32 bit, integer e long integer coincidono. 

Tutti i tipi sin qui descritti possono rappresentare solo valori interi, e sono perciò detti integral types

In C è naturalmente possibile gestire anche numeri in virgola mobile, mediante appositi tipi di dato[3]: il floating point, il double precision e il long double precision. Il floating point occupa 32 bit ed offre 7 cifre significative di precisione, il double precision occupa 64 bit con 15 cifre di precisione e il long double precision 80 bit[4] con 19 cifre di precisione. Tutti i tipi in virgola mobile sono dotati di segno. 

La tabella che segue riassume le caratteristiche dei tipi di dato sin qui descritti. 

TIPI DI DATO IN C 

TIPO
BIT
VALORI AMMESSI
Precisione
character
8
da -128 a 127
­
unsigned character
8
da 0 a 255
­
short integer
16
da -32768 a 32767
­
unsigned short integer
16
da 0 a 65535
­
integer
16
da -32768 a 32767
­
unsigned integer
16
da 0 a 65535
­
long integer
32
da -2147483648 a 2147483647 
­
unsigned long integer
32
da 0 a 4294967295
­
floating point
32
da 3.4*10-38 a 3.4*1038 
7 cifre
double precision
64
da 1.7*10-308 a 1.7*10308 
15 cifre
long double precision
80
da 3.4*10-4932 a 1.1*104932 
19 cifre

Il C non contempla un tipo di dato "stringa". Le stringhe di caratteri (come "Ciao Ciao!\n") sono gestite come array di character, cioè come sequenze di caratteri che occupano posizioni contigue in memoria ed ai quali è possibile accedere mediante l'indice della loro posizione. Le stringhe possono anche essere gestite mediante i puntatori

Vi è, infine, un tipo di dato particolare, utilizzabile per esprimere l'assenza di dati o per evitare di specificare a quale tipo, tra quelli appena descritti, appartenga il dato: si tratta del void type. Esso può essere utilizzato esclusivamente per dichiarare puntatori void e funzioni

Le variabili 

E' il momento di ripescare CIAO.C e complicarlo un poco. 

#include <stdio.h>

void main(void);

void main(void)
{
    unsigned int anni;
    float numero;

    anni = 31;
    numero = 15.66;
    printf("Ciao Ciao! Io ho %u anni\n",anni);
    printf("e questo è un float: %f\n",numero);
}

Nella nuova versione, CIAO2.C abbiamo introdotto qualcosa di molto importante: l'uso delle variabili. Il C consente di individuare una certa area di memoria mediante un nome arbitrario che le viene attribuito con un'operazione detta definizione della variabile; la variabile è ovviamente l'area di RAM così identificata. Ogni riferimento al nome della variabile è in realtà un riferimento al valore in essa contenuto; si noti, inoltre, che nella definizione della variabile viene specificato il tipo di dato associato a quel nome (e dunque contenuto nella variabile). In tal modo il programmatore può gestire i dati in RAM senza conoscerne la posizione e senza preoccuparsi (entro certi limiti) della loro dimensione in bit e dell'organizzazione interna dei bit, cioè del significato che ciascuno di essi assume nell'area di memoria assegnata alla variabile. 

Con la riga 

    unsigned int anni;

viene definita una variabile di nome anni e di tipo unsigned integer (intero senza segno): essa occupa perciò una word nella memoria dell'elaboratore e può assumere valori da 0 a 65535. Va osservato che alla variabile non è associato, per il momento, alcun valore: essa viene inizializzata con la riga 

    anni = 31;

che costituisce un'operazione di assegnazione: il valore 31 è assegnato alla variabile anni; l'operatore "=", in C, è utilizzato solo per le assegnazioni (che sono sempre effettuate da destra a sinistra), in quanto per il controllo di una condizione di uguaglianza si utilizza un operatore apposito ("=="). 

Tuttavia è possibile inizializzare una variabile contestualmente alla sua dichiarazione: 

    unsigned int anni = 31;

è, in C, un costrutto valido. 

Nella definizione di variabili di tipo integral, la parola int può essere sempre omessa, eccetto il caso in cui sia "sola": 

    unsigned anni = 31;     // OK! sinonimo di unsigned int
    long abitanti;          // OK! sinonimo di long int
    valore;                 // ERRORE! il solo nome della variabile NON basta!

Dal momento che ci siamo, anche se non c'entra nulla con le variabili, tanto vale chiarire che le due barre "//" introducono un commento, come si deduce dalle dichiarazioni appena viste. Viene considerato commento tutto ciò che segue le due barre, fino al termine della riga. Si possono avere anche commenti multiriga, aperti da "/*" e chiusi da "*/". Ad esempio: 

/* abbiamo esaminato alcuni esempi
   di dichiarazioni di variabili */

Tutto il testo che fa parte di un commento viene ignorato dal compilatore e non influisce sulle dimensioni del programma eseguibile; perciò è bene inserire con una certa generosità commenti chiarificatori nei propri sorgenti. Non è infrequente che un listato, "dimenticato" per qualche tempo, risulti di difficile lettura anche all'autore, soprattutto se questi non ha seguito la regola... KISS, già menzionata. I commenti tra "/*" e "*/" non possono essere nidificati, cioè non si può fare qualcosa come: 

/* abbiamo esaminato alcuni esempi
   /* alcuni validi e altri no */
   di dichiarazioni di variabili */

Il compilatore segnalerebbe strani errori, in quanto il commento è chiuso dal primo "*/" incontrato. 

Tornando all'argomento del paragrafo, va ancora precisato che in una riga logica possono essere definite (e, volendo, inizializzate) più variabili, purché tutte dello stesso tipo, separandone i nomi con una virgola: 

    int var1, var2;      // due variabili int, nessuna delle quali inizializzata
    char ch1 = 'A', ch2; // due variabili char, di cui solo la prima inizializ.
5    float num,                    // dichiarazione di 3 float ripartita su 3
          v1,                     // righe fisiche; solo l'ultima variabile
          terzaVar = 12.4;        // e' inizializzata

Soffermiamoci un istante sulla dichiarazione dei 3 float: la suddivisione in più righe non è obbligatoria, ed ha esclusivamente finalità di chiarezza (dove avremmo sistemato i commenti?). Inoltre, e questo è utile sottolinearlo, l'inizializzazione ha effetto esclusivamente sull'ultima variabile dichiarata, terzaVar. Un errore commesso frequentemente dai principianti (e dai distratti) è assegnare un valore ad una sola delle variabili dichiarate, nella convinzione che esso venga assegnato anche a tutte quelle dichiarate "prima". Ebbene, non è così. Ogni variabile deve essere inizializzata esplicitamente, altrimenti essa contiene... già... che cosa? Cosa contiene una variabile non inizializzata? Ai paragrafi successivi l'ardua sentenza... per il momento, provate a pensarci su. 

Inoltre, attenzione alle maiuscole. La variabile terzaVar deve essere sempre indicata con la "V" maiuscola: 

    int terzavar;        //OK!
    char TerzaVar;       //OK!
    double terzaVar;     //ERRORE! terzaVar esiste gia'!

Non è possibile dichiarare più variabili con lo stesso nome in una medesima funzione (ma in funzioni diverse sì). A rendere differente il nome è sufficiente una diversa disposizione di maiuscole e minuscole. 

I nomi delle variabili devono cominciare con una lettera dell'alfabeto o con l'underscore ("_") e possono contenere numeri, lettere e underscore. La loro lunghezza massima varia a seconda del compilatore; le implementazioni commerciali più diffuse  ammettono nomi composti di oltre32 caratteri

    double _numVar;
    int Variabile_Intera_1;
    char 1_carattere;             //ERRORE! il nome inizia con un numero

Anche il void type può essere incontrato nelle dichiarazioni: esso può però essere utilizzato solo per dichiarare funzioni o puntatori, ma non comuni variabili; la parola chiave da utilizzare nelle dichiarazioni è void

Per riassumere, ecco l'elenco dei tipi di dato e delle parole chiave da utilizzare per dichiarare le variabili. 

TIPI DI DATO E DICHIARATORI 

TIPO
DICHIARATORI VALIDI
characterchar
unsigned characterunsigned char 
short integershort int, short 
unsigned short integerunsigned short int, unsigned short 
integerint
unsigned integerunsigned int, unsigned 
long integerlong int, long 
unsigned long integerunsigned long int, unsigned long 
floating pointfloat 
double precision floating pointdouble 
long double precision floating pointlong double 
void typevoid

Un'ultima osservazione: avete notato che nelle stringhe passate a printf() sono comparsi strani simboli ("%u", "%f")? Si tratta di formattatori di campo e indicano a printf() come interpretare (e quindi visualizzare) le variabili elencate dopo la stringa stessa. La sequenza "%u" indica un intero senza segno, mentre "%f" indica un dato di tipo float. Un intero con segno si indica con "%d", una stringa con "%s", un carattere con "%c". 

Dalle pagine che precedono appare chiaro che la dimensione dell'area di memoria assegnata dal compilatore ad una variabile dipende dall'ingombro in byte del tipo di dato dichiarato. In molti casi può tornare utile sapere quanti byte sono allocati (cioè assegnati) ad una variabile, o a un tipo di dato. Allo scopo è possibile servirsi dell'operatore sizeof(), che restituisce come int il numero di byte occupato dal tipo di dato o dalla variabile indicati tra le parentesi. 

I puntatori 

Una variabile è un'area di memoria alla quale è associato un nome simbolico, scelto dal programmatore. Detta area di memoria è grande quanto basta per contenere il tipo di dato indicato nella dichiarazione della variabile stessa, ed è collocata dal compilatore, automaticamente, in una parte di RAM non ancora occupata da altri dati. La posizione di una variabile in RAM è detta indirizzo, o address. Possiamo allora dire che, in pratica, ad ogni variabile il compilatore associa sempre due valori: il dato in essa contenuto e il suo indirizzo, cioè la sua posizione in memoria. 

Gli indirizzi di memoria 

Proviamo ad immaginare la memoria come una sequenza di piccoli contenitori, ciascuno dei quali rappresenta un byte: ad ogni "contenitore", talvolta detto "locazione", potremo attribuire un numero d'ordine, che lo identifica univocamente. Se il primo byte ha numero d'ordine 0, allora il numero assegnato ad un generico byte ne individua la posizione in termini di spostamento (offset) rispetto al primo byte, cioè rispetto all'inizio della memoria. Così, il byte numero 12445 dista proprio 12445 byte dal primo, il quale, potremmo dire, dista 0 byte da se stesso. L'indirizzamento (cioè l'accesso alla memoria mediante indirizzi) avviene proprio come appena descritto: ogni byte è accessibile attraverso il suo offset rispetto ad un certo punto di partenza, il quale, però, non necessariamente è costituito dal primo byte di memoria in assoluto. Vediamo perché. 

Nella CPU del PC sono disponibili alcuni byte, organizzati come vere e proprie variabili, dette registri (register). La CPU è in grado di effettuare elaborazioni unicamente sui valori contenuti nei propri registri (che si trovano fisicamente al suo interno e non nella RAM); pertanto qualunque valore oggetto di elaborazione deve essere "caricato", cioè scritto, negli opportuni registri. Il risultato delle operazioni compiute dalla CPU deve essere conservato, se necessario, altrove (tipicamente nella RAM), al fine di lasciare i registri disponibili per altre elaborazioni. 

Anche gli indirizzi di memoria sono soggetti a questa regola. 

I registri del processore Intel 8086 si compongono di 16 bit ciascuno, pertanto il valore massimo che essi possono esprimere è quello dell'integer, cioè 65535 (esadecimale FFFF): il massimo offset gestibile dalla CPU permette dunque di indirizzare una sequenza di 65536 byte (compreso il primo, che ha offset pari a 0), corrispondenti a 64Kb. 

Configurazioni di RAM superiori (praticamente tutte) devono perciò essere indirizzate con uno stratagemma: in pratica si utilizzano due registri, rispettivamente detti registro di segmento (segment register) e registro di offset (offset register). Segmento e offset vengono solitamente indicati in notazione esadecimale, utilizzando i due punti (":") come separatore, ad esempio 045A:10BF. Ma non è tutto. 

Se segmento e offset venissero semplicemente affiancati, si potrebbero indirizzare al massimo 128Kb di RAM: infatti si potrebbe avere un offset massimo di 65535 byte a partire dal byte numero 65535. Quello che occorre è invece un valore in grado di numerare, o meglio di indirizzare, almeno 1Mb: i fatidici 640Kb, ormai presenti su tutte le macchine in circolazione, più gli indirizzi riservati al BIOS e alle  schede adattatrici[5]. Occorre, in altre parole, un indirizzamento a20 bit[6]

Questo si ottiene sommando al segmento i 12 bit più significativi dell'offset, ed accodando i 4 bit rimanenti dell'offset stesso: tale tecnica consente di trasformare un indirizzo segmento:offset in un  indirizzo lineare[7]. L'indirizzo seg:off di poco fa (045A:10BF) corrisponde all'indirizzo lineare 0565F, infatti 045A+10B = 565 (le prime 3 cifre di un valore esadecimale di 4 cifre, cioè a 16 bit, corrispondono ai 12 bit più significativi). 

Complicato? Effettivamente... Ma dal momento che le cose stanno proprio così, tanto vale adeguarsi e cercare di padroneggiare al meglio la situazione. In fondo è anche questione di abitudine. 

Gli operatori * e & 

Il C consente di pasticciare a volontà, ed anche... troppo, con gli indirizzi di memoria mediante particolari strumenti, detti puntatori, o pointers

Un puntatore non è altro che una normalissima variabile contenente un indirizzo di memoria. I puntatori non rappresentano un tipo di dato in sé, ma piuttosto sono tipizzati in base al tipo di dato a cui... puntano, cioè di cui esprimono l'indirizzo. Perciò essi sono dichiarati in modo del tutto analogo ad una variabile di quel tipo, anteponendo però al nome del puntatore stesso l'operatore "*", detto operatore di indirezione (dereference operator). 

Così, la riga 

    int unIntero;

dichiara una variabile di tipo int avente nome unIntero, mentre la riga 

    int *puntaIntero;

dichiara un puntatore a int avente nome puntaIntero (il puntatore ha nome puntaIntero, non l'int... ovvio!). E' importante sottolineare che si tratta di un puntatore a integer: il compilatore C effettua alcune operazioni sui puntatori in modo automaticamente differenziato a seconda del tipo che il puntatore  indirizza[8], ma è altrettanto importante non dimenticare mai che un puntatore contiene semplicemente un indirizzo (o meglio un valore che viene gestito dal compilatore come un indirizzo). Esso indirizza, in altre parole, un certo byte nella RAM; la dichiarazione del tipo "puntato" permette al compilatore di "capire" di quanti byte si compone l'area che inizia a quell'indirizzo e come è organizzata al proprio interno, cioè quale significato attribuire ai singoli bit. 

Si possono dichiarare più puntatori in un'unica riga logica, come del resto avviene per le variabili: la riga seguente dichiare tre puntatori ad intero. 

    int *ptrA, *ptrB, *ptrC;

Si noti che l'asterisco, o meglio, l'operatore di indirezione, è ripetuto davanti al nome di ogni puntatore. Se non lo fosse, tutti i puntatori dichiarati senza di esso sarebbero in realtà... normalissime variabili di tipo int. Ad esempio, la riga che segue dichiara due puntatori ad intero, una variabile intera, e poi ancora un puntatore ad intero. 

    int *ptrA, *ptrB, unIntero, *intPtr;

Come si vede, la dichiarazione mista di puntatori e variabili è un costrutto sintatticamente valido; occorre, come al solito, prestare attenzione a ciò che si scrive se si vogliono evitare errori logici piuttosto insidiosi. Detto tra noi, principianti e distratti sono i più propensi a dichiarare correttamente il primo puntatore e privare tutti gli altri dell'asterisco nella convinzione che il tipo dichiarato sia int*. In realtà, una riga di codice come quella appena riportata dichiara una serie di oggetti di tipo int; è la presenza o l'assenza dell'operatore di indirezione a stabilire, singolarmente per ciascuno di essi, se si tratti di una variabile o di un puntatore. 

Mediante l'operatore & (detto "indirizzo di", o address of) è possibile, inoltre, conoscere l'indirizzo di una variabile: 

    float numero;     // dichiara una variabile float
    float *numPtr;    // dichiara un puntatore ad una variabile float

    numero = 12.5;    // assegna un valore alla variabile
    numPtr = &numero; // assegna al puntatore l'indirizzo della variabile

E' chiaro il rapporto tra puntatori e variabili? Una variabile contiene un valore del tipo della dichiarazione, mentre un puntatore contiene l'indirizzo, cioè la posizione in memoria, di una variabile che a sua volta contiene un dato del tipo della dichiarazione. Dopo le operazioni dell'esempio appena visto, numPtr non contiene 12.5, ma l'indirizzo di memoria al quale 12.5 si trova. 

Anche un puntatore è una variabile, ma contiene un valore che non rappresenta un dato di un particolare tipo, bensì un indirizzo. Anche un puntatore ha il suo bravo indirizzo, ovviamente. Riferendosi ancora all'esempio precedente, l'indirizzo di numPtr può essere conosciuto con l'espressione &numPtr e risulta sicuramente diverso da quello di numero, cioè dal valore contenuto in numPtr. Sembra di giocare a rimpiattino... 

Proviamo a confrontare le due dichiarazioni dell'esempio: 

    float numero;
    float *numPtr;

Esse sono fortemente analoghe; del resto abbiamo appena detto che la dichiarazione di un puntatore è identica a quella di una comune variabile, ad eccezione dell'asterisco che precede il nome del puntatore stesso. Sappiamo inoltre che il nome attribuito alla variabile identifica un'area di memoria che contiene un valore del tipo dichiarato: ad esso si accede mediante il nome stesso della variabile, cioè il simbolo che, nella dichiarazione, si trova a destra della parola chiave che indica il tipo, come si vede chiaramente nell'esempio che segue. 

    printf("%f\n",numero);

L'accesso al valore della variabile avviene nella modalità appena descritta non solo in lettura, ma anche in scrittura: 

    numero = 12.5;

Cosa troviamo a destra dell'identificativo di tipo in una dichiarazione di puntatore? Il nome preceduto dall'asterisco. Ma allora anche il nome del puntatore con l'asterisco rappresenta un valore del tipo dichiarato... Provate ad immaginare cosa avviene se scriviamo: 

    printf("%f\n",*numPtr);

La risposta è: printf() stampa il  valore di numero[9]. In altre parole, l'operatore di indirezione non solo differenzia la dichiarazione di un puntatore da quella di una variabile, ma consente anche di accedere al contenuto della variabile (o, più in generale, della locazione di memoria) indirizzata dal puntatore. Forse è opportuno, a questo punto, riassumere il tutto con qualche altro esempio. 

    float numero = 12.5;
    float *numPtr = &numero;

Sin qui  nulla di nuovo[10]. Supponiamo ora che l'indirizzo di numero sia, in esadecimale, FFE6 e che quello di numPtr sia FFE4: non ci resta che giocherellare un po' con gli operatori address of ("&") e dereference ("*")... 

    printf("numero = %f\n",numero);
    printf("numero = %f\n",*numPtr);
    printf("l'indirizzo di numero e' %X\n",&numero);
    printf("l'indirizzo di numero e' %X\n",numPtr);
    printf("l'indirizzo di numPtr e' %X\n",&numPtr);

L'output prodotto è il seguente: 

numero = 12.5
numero = 12.5
l'indirizzo di numero è FFE6
l'indirizzo di numero è FFE6
l'indirizzo di numPtr è FFE4

Le differenza tra le varie modalità di accesso al contenuto e all'indirizzo delle veriabili dovrebbe ora essere chiarita. Almeno, questa è la speranza. Tra l'altro abbiamo imparato qualcosa di nuovo su printf(): per stampare un intero in formato esadecimale si deve inserire nella stringa, invece di %d, %X se si desidera che le cifre A­F siano visualizzate con caratteri maiuscoli, %x se si preferiscono i caratteri minuscoli. 

Va osservato che è prassi usuale esprimere gli indirizzi in notazione esadecimale. A prima vista può risultare un po' scomodo, ma, operando in tal modo, la logica di alcune operazioni sugli indirizzi stessi (e sui puntatori) risulta sicuramente più chiara. Ad esempio, ogni cifra di un numero esadecimale rappresenta quattro bit in memoria: si è già visto come ciò permetta di trasformare un indirizzo segmentato nel suo equivalente lineare con grande facilità. Per la cronaca, tale operazione è detta anche "normalizzazione" dell'indirizzo (o del puntatore). 

Vogliamo complicarci un poco la vita? Eccovi alcune interessanti domandine, qualora non ve le foste ancora posti... 

  1. Quale significato ha l'espressione *&numPtr
  2. Quale significato ha l'espressione **numPtr
  3. E l'espressione *numero
  4. E l'espressione &*numPtr
  5. &*numPtr e numPtr sono la stessa cosa? 
  6. Cosa restituisce l'espressione &&numero
  7. E l'espressione &&numPtr
  8. Cosa accade se si esegue *numPtr = 21.75
  9. Cosa accade se si esegue numPtr = 0x24A6
  10. E se si esegue &numPtr = 0xAF2B

Provate a trovare da soli le soluzioni, prima di andare a leggere le risposte

Complicazioni 

I puntatori sono, dunque, strumenti appropriati alla manipolazione ad alto livello degli indirizzi delle variabili. C'è proprio bisogno di preoccuparsi dei registri della CPU e di tutte le contorsioni possibili tra indirizzi seg:off e indirizzi lineari? Eh, sì... un poco è necessario; ora si tratta di capire il perché. 

Poco fa abbiamo ipotizzato che l'indirizzo di numero e di numPtr fossero, rispettivamente, FFE6 e FFE4. A prescindere dai valori, realisitci ma puramente ipotetici, è interessante notare che si tratta di due unsigned int. In effetti, per visualizzarli correttamente, abbiamo passato a printf() stringhe contenenti %X, lo specificatore di formato per gli interi in formato esadecimale. Che significa tutto ciò? 

Significa che il valore memorizzato in numPtr (e in  qualsiasi altro puntatore[11]) è una word, occupa 16 bit e si differenzia da un generico intero senza segno per il solo fatto che esprime un indirizzo di memoria. E' evidente, alla luce di quanto appena affermato, che l'indirizzo memorizzato in numPtr è un offset: come tutti i valori a 16 bit esso è gestito dalla CPU in uno dei suoi registri e può variare tra 0 e 65535. Un puntatore come numPtr esprime allora, in byte, la distanza di una variabile da... che cosa? Dall'indirizzo contenuto in un altro registro della CPU, gestito automaticamente dal compilatore. 

Con qualche semplificazione possiamo dire che il compilatore, durante la traduzione del sorgente in linguaggio macchina, stabilisce quanto spazio il programma ha a disposizione per gestire i propri dati e a quale distanza dall'inizio del codice eseguibile deve avere inizio l'area riservata ai dati. Dette informazioni sono memorizzate in una tabella, collocata in testa al file eseguibile, che il sistema operativo utilizza per caricare l'opportuno valore in un apposito registro della CPU. Questo registro contiene la parte segmento dell'indirizzo espresso da ogni puntatore dichiarato come numPtr

Nella maggior parte dei casi l'esistenza dei registri di segmento è del tutto trasparente al programmatore, il quale non ha alcun bisogno di proccuparsene, in quanto compilatore, linker e sistema operativo svolgono automaticamente tutte le operazioni necessarie alla loro gestione. Nello scrivere un programma è di solito sufficiente lavorare con i puntatori proprio come abbiamo visto negli esempi che coinvolgono numero e numPtr: gli operatori "*" e "&" sono caratterizzati da una notevole potenza operativa. 

Puntatori far e huge 

Le considerazioni sin qui espresse, però, aprono la via ad alcuni approfondimenti. In primo luogo, va sottolineato ancora una volta che numPtr occupa 16 bit di memoria, cioè 2 byte, proprio come qualsiasi unsigned int. E ciò è valido anche se il tipo di numero, la variabile puntata, è il float, che ne occupa 4. In altre parole, un puntatore occupa sempre lo spazio necessario a contenere l'indirizzo del dato puntato, e non il tipo di dato; tutti i puntatori come numPtr, dunque, occupano 2 byte, indipendentemente che il tipo di dato puntato sia un int, piuttosto che un float, o un double... Una semplice verifica empirica può essere effettuata con l'aiuto dell'operatore sizeof()

    int unIntero;
    long unLongInt;
    float unFloating;
    double unDoublePrec;

    int *intPtr;
    long *longPtr;
    float *floatPtr;
    double *doublePtr;

    printf("intPtr:    %d bytes (%d)\n",sizeof(intPtr),sizeof(int *));
    printf("longPtr:   %d bytes (%d)\n",sizeof(longPtr),sizeof(long *));
    printf("floatPtr:  %d bytes (%d)\n",sizeof(floatPtr),sizeof(float *));
    printf("doublePtr: %d bytes (%d)\n",sizeof(doublePtr),sizeof(double *));

Tutte le printf() visualizzano due volte il valore 2, che è appunto la dimensione in byte di un generico puntatore. L'esempio mostra, tra l'altro, come sizeof() possa essere applicato sia al tipo di dato che al nome di una variabile (in questo caso dei puntatori); se ne trae, infine, che il tipo di un puntatore è dato dal tipo di dato puntato, seguito dall'asterisco. 

Tutti i puntatori come numPtr, dunque, gestiscono un offset da un punto di partenza automaticamente fissato dal sistema operativo in base alle caratteristiche del file eseguibile. E' possibile in C, allora, gestire indirizzi lineari, o quanto meno comprensivi di segmento ed offset? La risposta è sì. Esistono due parole chiave, dette modificatori di tipo, che consentono di dichiarare puntatori speciali, in grado di gestire sia la parte segmento che la parte offset di un indirizzo di memoria: si tratta di far e huge

    double far *numFarPtr;

La riga di esempio dichiara un puntatore far a un dato di tipo double. Per effetto del modificatore far, numFarPtr è un puntatore assai differente dal numPtr degli esempi precedenti: esso occupa 32 bit di memoria, cioè 2 word, ed è pertanto equivalente ad un long int. Di conseguenza numFarPtr è in grado di esprimere tanto la parte offset di un indirizzo (nei 2 byte meno significativi), quanto la parte segmento (nei  2 byte più significativi[12]). La parte segmento è utilizzata dalla CPU per caricare l'opportuno registro di segmento, mentre la parte offset è gestita come al solito: in tal modo un puntatore far può esprimere un indirizzo completo del tipo segmento:offset e indirizzare dati che si trovano al di fuori dell'area dati assegnata dal sistema operativo al programma. 

Ad esempio, se si desidera che un puntatore referenzi l'indirizzo 596A:074B, lo si può dichiarare ed inizializzare come segue: 

    double far *numFarPtr = 0x596A074B;

Per visualizzare il contenuto di un puntatore far con printf() si può utilizzare un formattatore speciale: 

    printf("numFarPtr = %Fp\n",numFarPtr);

Il formattatore %Fp forza printf() a visualizzare il contenuto di un puntatore far proprio come segmento ed offset, separati dai due punti: 

numFarPtr = 596A:074B

è l'output prodotto dalla riga di codice appena riportata. 

Abbiamo appena detto che un puntatore far rappresenta un indirizzo seg:off. E' bene... ripeterlo qui, sottolineando che quell'indirizzo, in quanto seg:off, non è un indirizzo lineare. Parte segmento e parte offset sono, per così dire, indipendenti, nel senso che la prima è considerata costante, e la seconda variabile. Che significa? la riga 

    char far *vPtr = 0xB8000000;

dichiara un puntatore far a carattere e lo inizializza all'indirizzo B800:0000; la parte offset è nulla, perciò il puntatore indirizza il primo byte dell'area che ha inizio all'indirizzo lineare B8000 (a 20 bit). Il secondo byte ha offset pari a 1, perciò può essere indirizzato incrementando di 1 il puntatore, portandolo al valore 0xB8000001. Incrementando ancora il puntatore, esso assume valore 0xB8000002 e punta al terzo byte. Sommando ancora 1 al puntatore, e poi ancora 1, e poi ancora... si giunge ad un valore particolare, 0xB800FFFF, corrispondente all'indirizzo B800:FFFF, che è proprio quello del byte avente offset 65535 rispetto all'inizio dell'area. Esso è l'ultimo byte indirizzabile mediante un comune  puntatore near[13]. Che accade se si incrementa ancora vPtr? Contrariamente a quanto ci si potrebbe attendere, la parte offset si riazzera senza che alcun "riporto" venga sommato alla parte segmento. Insomma, il puntatore si "riavvolge" all'inizio dell'area individuata dall'indirizzo lineare rappresentato dalla parte segmento con uno 0 alla propria destra (che serve a costruire l'indirizzo a 20 bit). Ora si comprende meglio (speriamo!) che cosa si intende per parte segmento e parte offset separate: esse sono utilizzate proprio per caricare due distinti registri della CPU e pertanto sono considerate indipendenti l'una dall'altra, così come lo sono tra loro tutti i registri del microprocessore. 

Tutto ciò ha un'implicazione estremamente importante: con un puntatore far è possibile indirizzare un dato situato ad un qualunque indirizzo nella memoria disponibile entro il primo Mb, ma non è possibile "scostarsi" dall'indirizzo lineare espresso dalla parte segmento oltre i 64Kb. Per fare un esempio pratico, se si intende utilizzare un puntatore far per gestire una tabella, la dimensione complessiva di questa non deve eccedere i 64Kb. 

Tale limitazione è superata tramite il modificatore huge, che consente di avere puntatori in grado di indirizzare linearmente tutta la memoria disponibile (sempre entro il primo Mb). La dichiarazione di un puntatore huge non presenta particolarità: 

    int huge *iHptr;

Il segreto dei puntatori huge consiste in alcune istruzioni assembler che il compilatore introduce di soppiatto nei programmi tutte le volte che il valore del puntatore viene modificato o utilizzato, e che ne effettuano la normalizzazione. Con tale termine si indica un semplice calcolo che consente di esprimere l'indirizzo seg:off come rappresentazione di un indirizzo lineare: in modo, cioè, che la parte offset sia variabile unicamente da 0 a 15 (F esadecimale) ed i riporti siano sommati alla parte segmento. In pratica si tratta di sommare alla parte segmento i 12 bit più significativi della parte offset. Riprendiamo l'esempio precedente, utilizzando questa volta un puntatore huge

    char huge *vhugePtr = 0xB8000000;

L'inizializzazione del puntatore huge, come si vede, è identica a quella del puntatore far. Incrementando di 1 il puntatore si ottiene il valore 0xB8000001, come nel caso precedente. Sommando ancora 1 si ha 0xB8000002, e poi 0xB8000003, e così via. Sin qui, nulla di nuovo. Al quindicesimo incremento il puntatore vale 0xB800000F, come nel caso del puntatore far

Ma al sedicesimo incremento si manifesta la differenza: il puntatore far assume valore 0xB8000010, mentre il puntatore huge vale 0xB8010000: la parte segmento si è azzerata ed il 16 sottratto ad essa ha prodotto  un riporto[14] che è andato ad incrementare di 1 la parte segmento. Al trentunesimo incremento il puntatore far vale 0xB800001F, mentre quello huge è 0xB801000F. Al trentaduesimo incremento il puntatore far diventa 0xB8000020, mentre quello huge vale 0xB8020000

Il meccanismo dovrebbe essere ormai chiaro, così come il fatto che le prime 3 cifre della parte offset di un puntatore huge sono sempre 3 zeri. Fingiamo per un attimo di non vederli: la parte segmento e la quarta cifra della parte offset rappresentano proprio un indirizzo lineare a 20 bit. 

La normalizzazione effettuata dal compilatore consente di gestire indirizzi lineari pur caricando in modo indipendente parte segmento e parte offset in registri di segmento e, rispettivamente, di offset della CPU; in tal modo, con un puntatore huge non vi sono limiti né all'indirizzo di partenza, né alla quantità di memoria indirizzabile a partire da quell'indirizzo. Naturalmente ciò ha un prezzo: una piccola perdita di efficienza del codice eseguibile, introdotta dalla necessità di eseguire la routine di normalizzazione prima di utilizzare il valore del puntatore. 

Ancora una precisazione: nelle dichiarazioni multiple di puntatori far e huge, il modificatore deve essere ripetuto per ogni puntatore dichiarato, analogamente a quanto occorre per l'operatore di indirezione. L'omissione del modificatore determina la dichiarazione di un puntatore "offset" a 16 bit. 

    long *lptr, far *lFptr, lvar, huge *lHptr;

Nell'esempio sono dichiarati, nell'ordine, il puntatore a long a 16 bit lptr, il puntatore far a long lFptr, la variabile long lvar e il puntatore huge a long lHptr

E' forse il caso di sottolineare ancora che la dichiarazione di un puntatore riserva spazio in memoria esclusivamente per il puntatore stesso, e non per una variabile del tipo di dato indirizzato. Ad esempio, la dichiarazione 

    long double far *dFptr;

alloca, cioè riserva, 32 bit di RAM che potranno essere utilizzate per contenere l'indirizzo di un long double, i cui 80 bit dovranno essere allocati con  un'operazione a parte[15]

Tanto per confondere un poco le idee, occorre precisare un ultimo particolare. I sorgenti C possono essere compilati, tramite particolari opzioni riconosciute dal compilatore, in modo da applicare differenti criteri di default alla gestione dei puntatori. In particolare, vi sono modalità di compilazione che trattano tutti i puntatori come variabili a 32 bit, eccetto quelli esplicitamente dichiarati near. Ne riparleremo descrivendo i modelli di memoria

Per il momento è il caso di accennare a tre macro, definite in DOS.H, che agevolano in molti casi la manipolazione dei puntatori a 32 bit, siano essi far o huge: si tratta di MK_FP(), che "costruisce" un puntatore a 32 bit dati un segmento ed un offset entrambi a 16 bit, di FP_SEG(), che estrae da un puntatore a 32 bit i 16 bit esprimenti la parte segmento e di FP_OFF(), che estrae i 16 bit esprimenti l'offset. Vediamole al lavoro: 

#include <dos.h>
    ....
    unsigned farPtrSeg;
    unsigned farPtrOff;
    char far *farPtr;
    ....
    farPtr = (char far *)MK_FP(0xB800,0);  // farPtr punta a B800:0000
    farPtrSeg = FP_SEG(farPtr);    // farPtrSeg contiene 0xB800
    farPtrOff = FP_OFF(farPtr);    // farPtrOff contiene 0

Le macro testè descritte consentono di effettuare facilmente la normalizzzione di un puntatore, cioè trasformare l'indirizzo in esso contenuto in modo tale che la parte offset non sia superiore a 0Fh

    char far *cfPtr;
    char huge *chPtr;
    ....
    chPtr = (char huge *)(((long)FP_SEG(cfPtr)) << 16)+
        (((long)(FP_OFF(cfPtr) >> 4)) << 16)+(FP_OFF(cfPtr) & 0xF);

Come si vede, dalla parte offset sono scartati i 4 bit meno significativi: i 12 bit più significativi sono sommati al segmento; dalla parte offset sono poi scartati i 12 bit più significativi e i 4 bit restanti sono sommati al puntatore. Il significato degli operatori di shift << e >> e dell'operatore & (che in questo caso non ha il significato di  address of, ma di   and su bit) è descritto più avanti. 

L'indirizzo lineare corrispondente all'indirizzo segmentato espresso da un puntatore huge può essere ricavato come segue: 

    char huge *chPtr;
    long linAddr;
    ....
    linAddr = ((((((long)FP_SEG(chPtr)) << 16)+(FP_OFF(chPtr) << 12)) >> 12) &
        0xFFFFFL);

Per applicare tale algoritmo ad un puntatore far è necessario che questo sia dapprima normalizzato come descritto in precedenza. 

E' facile notare che due puntatori far possono referenziare il medesimo indirizzo pur contenendo valori a 32 bit differenti, mentre ciò non si verifica con i puntatori normalizzati, nei quali segmento e offset sono sempre gestiti in modo univoco: ne segue che solamente i confronti tra puntatori huge (o normalizzati) garantiscono risultati corretti. 

Puntatori static 

La dichiarazione 

    static float *ptr;

dichiara un puntatore static a un dato di tipo float. In realtà non è possibile, nel dichiarare un puntatore, indicare che esso indirizza un dato static essendo questo un modificatore della visibilità delle variabili, e non già del loro tipo. Si veda anche quanto detto circa i puntatori a funzione

Le stringhe 

Abbiamo anticipato che non esiste, in C, il tipo di dato "stringa". Queste sono gestite dal compilatore come sequenze di caratteri, cioè di dati di tipo char. Un metodo comunemente utilizzato per dichiarare e manipolare stringhe nei programmi è offerto proprio dai puntatori, come si vede nel programma dell'esempio seguente, che visualizza "Ciao Ciao!" e porta a capo il cursore. 

#include <stdio.h>

char *string = "Ciao";

void main(void)
{
    printf(string);
    printf(" %s!\n",string);
}

La dichiarazione di string può apparire, a prima vista, anomala. Si tratta infatti, a tutti gli effetti, della dichiarazione di un puntatore e la stranezza consiste nel fatto che a questo non è assegnato un indirizzo di memoria, come ci si potrebbe aspettare, bensì una costante stringa. Ma è proprio questo l'artificio che consente di gestire le stringhe con normali puntatori a carattere: il compilatore, in realtà, assegna a string, puntatore a 16 bit, l'indirizzo della costante "Ciao". Dunque la word occupata da string non contiene la parola "Ciao", ma i 16 bit che esprimono la parte offset del suo indirizzo. A sua volta, "Ciao" occupa 5 byte di memoria. Proprio 5, non si tratta di un errore di stampa: i 4 byte necessari a memorizzare i 4 caratteri che compongono la parola, più un byte, nel quale il compilatore memorizza il valore binario 0, detto terminatore di stringa o null terminator. In C, tutte le stringhe sono chiuse da un null terminator, ed occupano perciò un byte in più del numero di caratteri "stampabili" che le compongono. 

La prima chiamata a printf() passa quale argomento proprio string: dunque la stringa parametro indispensabile di printf() non deve essere necessariamente una stringa di formato quando l'unica cosa da visualizzare sia proprio una stringa. Lo è, però, quando devono essere visualizzati caratteri o numeri, o stringhe formattate in un modo particolare, come avviene nella seconda chiamata. 

Qui va sottolineato che per visualizzare una stringa con printf() occore fornirne l'indirizzo, che nel nostro caso è il contenuto del puntatore string. Se string punta alla stringa "Ciao", che cosa restituisce l'espressione *string? La tentazione di rispondere "Ciao" è forte, ma se così fosse perché per visualizzare la parola occorre passare a printf() string e non *string? Il problema non si poneva con gli esempi precedenti, perché tutti i puntatori esaminati indirizzavano un unico dato di un certo tipo. Con le dichiarazioni 

    float numero = 12.5;
    float *numPtr = &numero;

si definisce il puntatore numPtr e lo si inizializza in modo che contenga l'indirizzo della variabile numero, la quale, in fondo proprio come string, occupa più di un byte. In questo caso, però, i 4 byte di numero contengono un dato unitariamente considerato. In altre parole, nessuno dei 4 byte che la compongono ha significato in sé e per sé. Con riferimento a string, al contrario, ogni byte è un dato a sé stante, cioè un dato di tipo char: bisogna allora precisare che un puntatore indirizza sempre il primo byte di tutti quelli che compongono il tipo di dato considerato, se questi sono più d'uno. Se ne ricava che string contiene, in realtà, l'indirizzo del primo carattere di "Ciao", cioè la 'C'. Allora *string non può che restituire proprio quella, come si può facilmente verificare con la seguente chiamata a printf()

    printf("%c è il primo carattere...\n",*string);

Non dimentichiamo che le stringhe sono, per il compilatore C, semplici sequenze di char: la stringa del nostro esempio inizia con il char che si trova all'indirizzo contenuto in string (la 'C') e termina con il primo byte nullo incontrato ad un indirizzo uguale o superiore a quello (in questo caso il byte che segue immediatamente la 'o'). 

Per accedere ai caratteri che seguono il primo è sufficiente incrementare il puntatore o, comunque, sommare ad esso una opportuna quantità (che rappresenta l'offset, cioè lo spostamento, dall'inizo della stringa stessa). Vediamo, come al solito, un esempio: 

    int i = 0;

    while(*(string+i) != 0) {
        printf("%c\n",*(string+i));
        ++i;
    }

L'esempio si basa sull'aritmetica dei puntatori, cioè sulla possibilità di accedere ai dati memorizzati ad un certo offset rispetto ad un indirizzo sommandovi algebricamente numeri interi. Il ciclo visualizza la stringa "Ciao" in senso verticale. Infatti l'istruzione while (finalmente una "vera" istruzione C!) esegue le istruzioni comprese tra le parentesi graffe finché la condizione espressa tra le parentesi tonde è vera (se questa è falsa la prima volta, il ciclo non viene mai eseguito): in questo caso la printf() è eseguita finché il byte che si trova all'indirizzo contenuto in string aumentato di i unità è diverso da 0, cioè finché non viene incontrato il null terminator. La printf() visualizza il byte a quello stesso indirizzo e va a capo. Il valore di i è inizialmente 0, pertanto nella prima iterazione l'indirizzo espresso da string non è modificato, ma ad ogni loop i è incrementato di 1 (tale è il significato dell'operatore ++, pertanto ad ogni successiva iterazione l'espressione string+i restituisce l'indirizzo del byte successivo a quello appena visualizzato. Al termine, i contiene il valore 4, che è anche la lunghezza della stringa: questa è infatti convenzionalmente pari al numero dei caratteri stampabili che compongono la stringa stessa; il null terminator non viene considerato. In altre parole la lunghezza di una stringa è inferiore di 1 al numero di byte che essa occupa effettivamente in memoria. La lunghezza di una stringa può quindi essere calcolata così: 

    unsigned i = 0;

    while(*(string+i))
        ++i;

La condizione tra parentesi è implicita: non viene specificato alcun confronto. In casi come questo il compilatore assume che il confronto vada effettuato con il valore 0, che è proprio quel che fa al nostro caso. Inoltre, dato che il ciclo si compone di una sola riga (l'autoincremento di i), le graffe non sono necessarie (ma potrebbero essere  utilizzate ugualmente[16]). 

Tutta questa chiacchierata dovrebbe avere reso evidente una cosa: quando ad una funzione viene passata una costante stringa, come in 

    printf("Ciao!\n");

il compilatore, astutamente, memorizza la costante da qualche parte (non preoccupiamoci del "dove", per il momento) e ne passa l'indirizzo. 

Inoltre, il metodo visto poco fa per "prelevare" uno ad uno i caratteri che compongono una stringa vale anche nel caso li si voglia modificare: 

char *string = "Rosso\n";

void main(void)
{
    printf(string);
    *(string+3) = 'p';
    printf(string);
}

Il programma dell'esempio visualizza dapprima la parola "Rosso" e poi "Rospo". Si noti che il valore di string non è mutato: esso continua a puntare alla medesima locazione di memoria, ma è mutato il contenuto del byte che si trova ad un offset di 3 rispetto a quell'indirizzo. Dal momento che l'indirezione di un puntatore a carattere restituisce un carattere, nell'assegnazione della lettera 'p' è necessario esprimere quest'ultima come un char, e pertanto tra apici (e non tra virgolette). La variabile string non a caso è dichiarata all'esterno di main()

E' possibile troncare una stringa? Sì, basta inserire un NULL dove occorre: 

    *(string+2) = NULL;

A questo punto una chiamata a printf() visualizzerebbe la parola "Ro". NULL è una costante manifesta definita in STDIO.H, e rappresenta lo zero binario; infatti la riga di codice precedente potrebbe essere scritta così: 

    *(string+2) = 0;

E' possibile allungare una stringa? Sì, basta... essere sicuri di avere spazio a disposizione. Se si sovrascrive il NULL con un carattere, la stringa si allunga sino al successivo NULL. Occorre fare alcune considerazioni: in primo luogo, tale operazione ha senso, di solito, solo nel caso di concatenamento di stringhe (quando cioè si desidera accodare una stringa ad un'altra per produrne una sola, più lunga). In secondo luogo, se i byte successivi al NULL sono occupati da altri dati, questi vengono perduti, sovrascritti dai caratteri concatenati alla stringa: l'effetto può essere disastroso. In effetti esiste una funzione di libreria concepita appositamente per concatenare le stringhe: la strcat(), che richiede due stringhe quali parametri. L'azione da essa svolta consiste nel copiare i byte che compongono la seconda stringa, NULL terminale compreso, in coda alla prima stringa, sovrascrivendone il NULL terminale. 

In una dichiarazione come quella di string, il compilatore riserva alla stringa lo spazio strettamente necessario a contenere i caratteri che la compongono, più il NULL. E' evidente che concatenare a string un'altra stringa sarebbe un grave errore (peraltro non segnalato dal compilatore, perché esso lascia il programmatore libero di gestire la memoria come crede: se sbaglia, peggio per lui). Allora, per potere concatenare due stringhe senza pericoli occorre riservare in anticipo lo spazio necessario a contenere la prima stringa e la seconda... una in fila all'altra. Affronteremo il problema parlando di array e di allocazione dinamica della memoria

Avvertenza: una dichiarazione del tipo: 

    char *sPtr;

riserva in memoria lo spazio sufficiente a memorizzare il puntatore alla stringa, e non una (ipotetica) stringa. I byte allocati sono 2 se il puntatore è, come nell'esempio, near; mentre sono 4 se è far o huge. In ogni caso va ricordato che prima di copiare una stringa a quell'indirizzo bisogna assolutamente allocare lo spazio necessario a contenerla e assegnarne l'indirizzo a sPtr. Anche a questo proposito occorre rimandare gli approfondimenti alle pagine in cui esamineremo l'allocazione dinamica della memoria

E' meglio sottolineare che le librerie standard del C comprendono un gran numero di funzioni (dichiarate in STRING.H) per la manipolazione delle stringhe, che effettuano le più svariate operazioni: copiare stringhe o parte di esse (strcpy(), strncpy()), concatenare stringhe (strcat(), strncat()), confrontare stringhe (strcmp(), stricmp()), ricercare sottostringhe o caratteri all'interno di stringhe (strstr(), strchr(), strtok())... insomma, quando si deve trafficare con le stringhe vale la pena di consultare il manuale delle librerie e cercare tra le funzioni il cui nome inizia con "str": forse la soluzione al problema è già pronta. 

Gli array 

Un array (o vettore) è una sequenza di dati dello stesso tipo, sistemati in memoria... in fila indiana. Una stringa è, per definizione, un array di char. Si possono dichiarare array di int, di double, o di qualsiasi altro tipo. Il risultato è, in pratica, riservare in memoria lo spazio necessario a contenere un certo numero di variabili di quel tipo. In effetti, si può pensare ad un array anche come ad un gruppo di variabili, aventi tutte identico nome ed accessibili, quindi, referenziandole attraverso un indice. Il numero di "variabili" componenti l'array è indicato nella dichiarazione: 

    int iArr[15];

La dichiarazione di un array è analoga a quella di una variabile, ad eccezione del fatto che il nome dell'array è seguito dal numero di elementi che lo compongono, racchiuso tra parentesi quadre. Quella dell'esempio forza il compilatore a riservare lo spazio necessario a memorizzare 15 interi, dunque 30 byte. Per accedere a ciascuno di essi occorre sempre fare riferimento al nome dell'array, iArr: il singolo int desiderato è individuato da un indice tra parentesi quadre, che ne indica la posizione. 

    iArr[0] = 12;
    iArr[1] = 25;
    for(i = 2; i < 15; i++)
        iArr[i] = i;
    for(i = 0; i < 15;) {
        printf("iArr[%d] = %d\n",i,iArr[i]);
        i++;
    }

Nell'esempio i primi due elementi dell'array sono inizializzati a 12 e 25, rispettivamente. Il primo ciclo for inizializza i successivi elementi (dal numero 2 al numero 14) al valore che i assume ad ogni iterazione. Il secondo ciclo for visualizza tutti gli elementi dell'array. Preme sottolineare che gli elementi di un array sono numerati a partire da 0 (e non da 1), come ci si potrebbe attendere. Dunque, l'ultimo elemento di un array ha indice inferiore di 1 rispetto al numero di elementi in esso presenti. Si vede chiaramente che gli elementi di iArr, dichiarato come array di 15 interi, sono referenziati con indice che va da 0 a 14

Che accade se si tenta di referenziare un elemento che non fa parte dell'array, ad esempio iArr[15]? Il compilatore non fa una grinza: iArr[15] può essere letto e scritto tranquillamente... E' ovvio che nel primo caso (lettura) il valore letto non ha alcun significato logico ai fini del programma, mentre nel secondo caso (scrittura) si rischia di perdere (sovrascrivendolo) qualche altro dato importante. Anche questa volta il compilatore si limita a mettere a disposizione del programmatore gli strumenti per gestire la memoria, senza preoccuparsi di controllarne più di tanto l'operato. Per il compilatore, iArr[15] è semplicemente la word che si trova a 30 byte dall'indirizzo al quale l'array è memorizzato. Che farne, è affare del programmatore[17]

Un array, come qualsiasi altro oggetto in memoria, ha un indirizzo. Questo è individuato e scelto dal compilatore. Il programmatore non può modificarlo, ma può conoscerlo attraverso il nome dell'array stesso, usandolo come un puntatore. In C, il nome di un array equivale, a tutti gli effetti, ad un puntatore all'area di memoria assegnata all'array. Pertanto, le righe di codice che seguono sono tutte lecite: 

    int *iPtr;

    printf("indirizzo di iArr: %X\n",iArr);
    iPtr = iArr;
    printf("indirizzo di iArr: %X\n",iPtr);
    printf("primo elemento di iArr: %d\n",*iArr);
    printf("secondo elemento di iArr: %d\n",*(iArr+1));
    ++iPtr;
    printf("secondo elemento di iArr: %d\n",*iPtr);

mentre non sono lecite le seguenti: 

    ++iArr;         // l'indirizzo di un array non puo' essere modificato
    iArr = iPtr;    // idem

ed è lecita, ma inutilmente complessa, la seguente: 

    iPtr = &iArr;

in quanto il nome dell'array ne restituisce, di per se stesso, l'indirizzo, rendendo inutile l'uso dell'operatore & (address of). 

Il lettore attento dovrebbe avere notato che l'indice di un elemento di un array ne esprime l'offset, in termini di numero di elementi, dal primo elemento dell'array stesso. In altre parole, il primo elemento di un array ha offset 0 rispetto a se stesso; il secondo ha offset 1 rispetto al primo; il terzo ha offset 2, cioè dista 2 elementi dal primo... 

Banale? Mica tanto. Il compilatore "ragiona" sugli arrays in termini di elementi, e non di byte. 

Ripensando alle stringhe, appare ora evidente che esse non sono altro che array di char. Si differenziano solo per l'uso delle virgolette; allora il problema del concatenamento di stringhe può essere risolto con un array: 

char string[100];

Nell'esempio abbiamo così a disposizione 100 byte in cui copiare e concatenare le nostre stringhe. 

Puntatori ed array hanno caratteristiche fortemente simili. Si differenziano perché ad un array non può essere  assegnato un valore[18], e perché un array riserva direttamente, come si è visto, lo spazio necessario a contenere i suoi elementi. Il numero di elementi deve essere specificato con una costante. Non è mai possibile utilizzare una variabile. Con una variabile, utilizzata come indice, si può solo accedere agli elementi dell'array dopo che questo è stato dichiarato. 

Gli array, se dichiarati al di fuori di qualsiasi  funzione[19], possono essere inizializzati: 

int iArr[] = {12,25,66,0,144,-2,26733};
char string[100] = {'C','i','a','o'};
float fArr[] = {1.44,,0.3};

Per inizializzare un array contestualmente alla dichiarazione bisogna specificare i suoi elementi, separati da virgole e compresi tra parentesi graffe aperta e chiusa. Se non si indica tra le parentesi quadre il numero di elementi, il compilatore lo desume dal numero di elementi inizializzati tra le parentesi graffe. Se il numero di elementi è specificato, e ne viene inizializzato un numero inferiore, tutti quelli "mancanti" verranno inizializzati a 0 dal compilatore. Analoga regola vale per gli elementi "saltati" nella lista di inizializzazione: l'array fArr contiene 3 elementi, aventi valore 1.44, 0.0 e 0.3 rispettivamente. 

Su string si può effettuare una concatenazione come la seguente senza rischi: 

    strcat(string," Pippo");

La stringa risultante, infatti, è "Ciao Pippo", che occupa 11 byte compreso il NULL terminale: sappiamo però di averne a disposizione 100. 

Sin qui si è parlato di array monodimensionali, cioè di array ogni elemento dei quali è referenziabile mediante un solo indice. In realtà, il C consente di gestire array multidimensionali, nei quali per accedere ad un elemento occorre specificarne più "coordinate". Ad esempio: 

       int iTab[3][6];

dichiara un array a 2 dimensioni, rispettivamente di 3 e 6 elementi. Per accedere ad un singolo elemento bisogna, allo stesso modo, utilizzare due indici: 

    int i, j, iTab[3][6];

    for(i = 0; i < 3; ++i)
        for(j = 0; j < 6; ++j)
            iTab[i][j] = 0;

Il frammento di codice riportato dichiara l'array bidimensionale iTab e ne inizializza a 0 tutti gli elementi. I due cicli for sono nidificati, il che significa che le iterazioni previste dal secondo vengono compiute tutte una volta per ogni iterazione prevista dal primo. In tal modo vengono "percorsi" tutti gli elementi di iTab. Infatti il modo in cui il compilatore C alloca lo spazio di memoria per gli array multidimensionali garantisce che per accedere a tutti gli elementi nella stessa sequenza in cui essi si trovano in memoria, è l'indice più a destra quello che deve variare più frequentemente. 

E' evidente, d'altra parte, che la memoria è una sequenza di byte: ciò implica che pur essendo iTab uno strumento che consente di rappresentare molto bene una tabella di 3 righe e 6 colonne, tutti i suoi elementi stanno comunque "in fila indiana". Pertanto, l'inizializzazione di un array multidimensionale contestuale alla sua dichiarazione può essere effettuata come segue: 

int *tabella[2][5] = {{3, 2, 0, 2, 1},{3, 0, 0, 1, 0}};

Gli elementi sono elencati proprio nell'ordine in cui si trovano in memoria; dal punto di vista logico, però, ogni gruppo di elementi nelle coppie di graffe più interne rappresenta una riga. Dal momento che, come già sappiamo, il C è molto elastico nelle regole che disciplinano la stesura delle righe di codice, la dichiarazione appena vista può essere spezzata su due righe, al fine di rendere ancora più evidente il parallelismo concettuale tra un array bidimensionale ed una tabella a doppia entrata: 

int *tabella[2][5] = {{3, 2, 0, 2, 1},
                      {3, 0, 0, 1, 0}};

Si noti che tra le parentesi quadre, inizializzando l'array contestualmente alla dichiarazione, non è necessario specificare entrambe le dimensioni, perché il compilatore può desumere quella mancante dal computo degli elementi inizialiazzati: nella dichiarazione dell'esempio sarebbe stato lecito scrivere tabella[][5] o tabella[2][]

Dalle affermazioni fatte discende infoltre che gli elementi di un array bidimensionale possono essere referenziati anche facendo uso di un solo indice: 

    int *iPtr;

    iPtr = tabella;
    for(i = 0; i < 2*5; i++)
        printf("%d\n",iPtr[i];

In genere i compilatori C sono in grado di gestire array multidimensionali senza un limite teorico (a parte la disponibilità di memoria) al numero di dimensioni. E' tuttavia infrequente, per gli utilizzi più comuni, andare oltre la terza dimensione. 

L'aritmetica dei puntatori 

Quanti byte di memoria occupa un array? La risposta dipende, ovviamente, dal numero degli elementi e dal tipo di dato dichiarato. Un array di 20 interi occupa 40 byte, dal momento che ogni int ne occupa 2. Un array di 20 long ne occupa, dunque, 80. Calcoli analoghi occorrono per accedere ad uno qualsiasi degli elementi di un array: il terzo elemento di un array di long ha indice 2 e dista 8 byte (2*4) dall'inizio dell'area di RAM riservata all'array stesso. Il quarto elemento di un array di int dista 3*2 = 6 byte dall'inizio dell'array. Generalizzando, possiamo affermare che un generico elemento di un array di un qualsiasi tipo dista dall'indirizzo base dell'array stesso un numero di byte pari al prodotto tra il proprio indice e la dimensione del tipo di dato. 

Fortunatamente il compilatore C consente di accedere agli elementi di un array in funzione di un unico parametro: il loro  indice[20]. Per questo sono lecite e significative istruzioni come quelle già viste: 

    iArr[1] = 12;
    printf("%X\n",iArr[j]);

E' il compilatore ad occuparsi di effettuare i calcoli sopra descritti per ricavare il giusto offset in termini di byte di ogni elemento, e lo fa in modo trasparente al programmatore per qualsiasi tipo di dato. 

Ciò vale anche per le stringhe (o array di caratteri). Il fatto che ogni char occupi un byte semplifica i calcoli ma non modifica i termini del  problema[21]

E' importante sottolineare che quanto affermato vale non solo nei confronti degli array, bensì di qualsiasi puntatore, come può chiarire l'esempio che segue. 

#include <stdio.h>

int iArr[]= {12,99,27,0};

void main(void)
{
    int *iPtr;

    iPtr = iArr;   // punta al primo elemento di iArr[]
    while(*iPtr) {        // finche' l'int puntato da iPtr non e' 0
        printf("%X -> %d\n",iPtr,*iPtr);      // stampa iPtr e l'intero puntato
        ++iPtr;        // incrementa iPtr
    }
}

Il trucco sta tutto nell'espressione ++iPtr: l'incremento del puntatore è automaticamente effettuato dal compilatore sommando 2 al valore contenuto in iPtr, proprio perché esso è un puntatore ad int, e l'int occupa 2 byte. In altre parole, iPtr è incrementato, ad ogni iterazione, in modo da puntare all'intero successivo. 

Si noti che l'aritmetica dei puntatori è applicata dal compilatore ogni volta che una grandezza intera è sommata a (o sottratta da) un puntatore, moltiplicando tale grandezza per il numero di byte occupati dal tipo di dato puntato. 

Questo modo di gestire i puntatori ha due pregi: da un lato evita al programmatore lo sforzo di pensare ai dati in memoria in termini di numero di byte; dall'altro consente la portabilità dei programmi che fanno uso di puntatori anche su macchine che codificano gli stessi tipi di dato con un diverso numero di bit. 

Un'ultima precisazione: ai putatori possono essere sommate o sottratte solo grandezze intere (int o long, a seconda che si tratti di puntatori near o no). 

Puntatori a puntatori 

Un puntatore è una variabile che contiene un indirizzo. Perciò è lecito (e, tutto sommato, abbastanza normale) fare uso di puntatori che puntano ad altri puntatori. La dichiarazione di un puntatore a puntatore si effettua così: 

char **pPtr;

In pratica occorre aggiungere un asterisco, in quanto siamo ad un secondo livello di indirezione: pPtr non punta direttamente ad un char; la sua indirezione *pPtr restituisce un altro puntatore a char, la cui indirezione, finalmente, restituisce il char agognato. Presentando i puntatori è stato analizzato il significato di alcune espressioni; in particolare si è detto che in **numPtr, ove numPtr è un puntatore a float, il primo "*" è ignorato: l'affermazione è corretta, perché pur essendo numPtr e pPtr entrambi puntatori, il secondo punta ad un altro puntatore, al quale può essere validamente applicato il primo dereference operator ("*"). 

L'ambito di utilizzo più frequente dei puntatori a puntatori è forse quello degli array di stringhe: dal momento che in C una stringa è di per sé un array (di char), gli array di stringhe sono gestiti come array di puntatori a char. A questo punto è chiaro che il nome dell'array (in C il nome di un array è anche puntatore all'array stesso) è un  puntatore a puntatori a char[22]. Pertanto, ad esempio, 

    printf(pPtr[2]);

visualizza la stringa puntata dal terzo elemento di pPtr (con una semplificazione "umana" ma un po' pericolosa potremmo dire che viene visualizzata la terza stringa dell'array). 

Puntatori void 

Un puntatore può essere dichiarato di tipo void. Si tratta di una pratica poco diffusa, avente lo scopo di lasciare indeterminato il tipo di dato che il puntatore indirizza, sino al momento dell'inizializzazione del puntatore stesso. La forma della dichiarazione è intuibile: 

void *ptr, far *fvptr;

Ad un puntatore void può essere assegnato l'indirizzo di qualsiasi tipo di dato. 

L'accessibilità e la durata delle variabili 

In C le variabili possono essere classificate, oltre che secondo il tipo di dato, in base alla loro accessibilità e alla loro durata. In particolare, a seconda del contesto in cui sono dichiarate, le variabili di un programma C assumono per default determinate caratteristiche di accessibilità e durata; in molti casi, però, queste possono essere modificate mediante l'utilizzo di apposite parole chiave applicabili alla dichiarazione delle variabili stesse. 

Per comprendere i concetti di accessibilità (o visibilità) e durata, va ricordato che una variabile altro non è che un'area di memoria, grande quanto basta per contenere un dato del tipo indicato nella dichiarazione, alla quale il compilatore associa, per comodità del programmatore, il nome simbolico da questi scelto. 

In termini generali, possiamo dire che la durata di una variabile si estende dal momento in cui le viene effettivamente assegnata un'area di memoria fino a quello in cui quell'area è riutilizzata per altri scopi. 

Dal punto di vista dell'accessibilità ha invece rilevanza se sia o no possibile leggere o modificare, da parti del programma diverse da quella in cui la variabile è stata dichiarata, il contenuto dell'area di RAM riservata alla variabile stessa 

Cerchiamo di mettere un po' d'ordine... 

Le variabili automatic 

Qualsiasi variabile dichiarata all'interno di un blocco di codice racchiuso tra parentesi graffe (generalmente all'inizio di una funzione) appartiene per default alla classe automatic. Non è dunque necessario, anche se è possibile farlo, utilizzare la parola chiave auto. La durata e la visibilità della variabile sono entrambe limitate al blocco di codice in cui essa è dichiarata. Se una variabile è dichiarata in testa ad una funzione, essa esiste (cioè occupa memoria) dal momento in cui la funzione inizia ad essere eseguita, sino al momento in cui la sua esecuzione termina. 

Le variabili automatic, dunque, non occupano spazio di memoria se non quando effettivamente servono; inoltre, essendo accessibili esclusivamente dall'interno di quella funzione, non vi è il rischio che possano essere modificate accidentalmente da operazioni svolte in funzioni diverse su variabili aventi medesimo nome: in un programma C, infatti, più variabili automatic possono avere lo stesso nome, purché dichiarate in blocchi di codice diversi. Se i blocchi sono nidificati (cioè uno è interamente all'interno di un altro) ciò è ancora vero, ma la variabile dichiarata nel blocco interno "nasconde" quella dichiarata con identico nome nel blocco esterno (quando, ovviamente, viene eseguito il blocco interno). 

Vediamo un esempio: 

#include <stdio.h>

void main(void)
{
    int x = 1;
    int y = 10;
    {
        int x = 2;

        printf("%d, %d\n",x,y);
    }
    printf("%d, %d\n",x,y);
}

La variabile x dichiarata in testa alla funzione main() è inizializzata a 1, mentre la x dichiarata nel blocco interno è inizializzata a 2. L'output del programma é: 

2, 10
1, 10

Ciò prova che la "prima" x esiste in tutta la funzione main(), mentre la "seconda" esiste ed è visibile solo nel blocco più interno; inoltre, dal momento che le due variabili hanno lo stesso nome, nel blocco interno la prima x è resa non visibile dalla seconda. La y, invece, è visibile anche nel blocco interno. 

Se si modifica il programma dell'esempio come segue: 

#include <stdio.h>

void main(void)
{
    int x = 1;
    int y = 10;
    {
        int x = 2;
        int z = 20;

        printf("%d, %d\n",x,y);
    }
    printf("%d, %d\n",x,y,z);
}

il compilatore non porta a termine la compilazione e segnala l'errore con un messaggio analogo a "undefined symbol z in function main()" a significare che la seconda printf() non può referenziare la variabile z, poiche questa cessa di esistere al termine del blocco interno di codice. 

La gestione delle variabili automatic è dinamica. La memoria necessaria è allocata alla variabile esclusivamente quando viene eseguito il blocco di codice (tipicamente una funzione) in cui essa è dichiarata, e le viene "sottratta" non appena il blocco termina. Ciò implica che non è possibile conoscere il contenuto di una variabile automatic prima che le venga esplicitamente assegnato un valore da una istruzione facente parte del blocco: a beneficio dei distratti, vale la pena di evidenziare che una variabile automatica può essere utilizzata in lettura prima di essere  inizializzata[23], ma il valore in essa contenuto è casuale e, pertanto, inutilizzabile nella quasi totalità dei casi. 

E' opportuno sottolineare che mentre le variabili dichiarate nel blocco più esterno di una funzione (cioè in testa alla stessa) esistono e sono visibili (salvo il caso di variabili con lo stesso nome) in tutti i blocchi interni di quella funzione, nel caso di funzioni diverse nessuna di esse può accedere alle variabili automatiche delle altre. 

Le variabili register 

Dal momento che il compilatore colloca le variabili automatic nella RAM del calcolatore, i valori in esse contenuti devono spesso essere copiati nei registri della CPU per poter essere elaborati e, se modificati dall'elaborazione subita, copiati nuovamente nelle locazioni di memoria di provenienza. Tali operazioni sono svolte in modo trasparente per il programmatore, ma possono deteriorare notevolmente la performance di un programma, soprattutto se ripetute più e più volte (ad esempio all'interno di un ciclo con molte iterazioni). 

Dichiarando una variabile automatic con la parola chiave register si forza il compilatore ad allocarla direttamente in un registro della CPU, con notevole incremento di efficienza nell'elaborazione del valore in essa contenuto. Ecco un esempio: 

    register int i = 10;

    do {
        printf("%2d\n",i);
    } while(i--);

Il ciclo visualizza,  incolonnati[24], i numeri da 10 a 0; la variabile i si comporta come una qualsiasi variabile automatic, ma essendo probabilmente gestita in un registro consente un'elaborazione più veloce. E' d'obbligo scrivere "probabilmente gestita" in quanto non si può essere assolutamente certi che il compilatore collochi una variabile dichiarata con register proprio in un registro della CPU: in alcune situazioni potrebbe gestirla come una variabile automatic qualsiasi, allocandola in memoria. I principali motivi sono due: la variabile potrebbe occupare più byte di quanti compongono un registro della  CPU[25], o potrebbero non esserci  registri disponibili allo scopo[26]

Già che ci siamo, diamo un'occhiata più approfondita all'esempio di poco fa. Innanzitutto va rilevato che nella dichiarazione di i potrebbe essere omessa la parola chiave int

    register i = 10;

Abbiamo poi utilizzato un costrutto nuovo: il ciclo do...while. Esso consente di identificare un blocco di codice (quello compreso tra le graffe) che viene eseguito finché la condizione specificata tra parentesi dopo la parola chiave while continua ad essere vera. Il ciclo viene sempre eseguito almeno una volta, perché il test è effettuato al termine del medesimo. Nel nostro caso, quale test viene effettuato? Dal momento che non è utilizzato alcun operatore di confronto esplicito, viene controllato se il risultato dell'espressione nelle tonde è diverso da 0. L'operatore --, detto di autodecremento, è specificato dopo la variabile a cui è applicato. Ciò assicura che i sia decrementata dopo l'effettuazione del test. Perciò il ciclo è eseguito 11 volte, con i che varia da 10 a 0 inclusi. Se l'espressione fosse --i, il decremento sarebbe eseguito prima del test, con la conseguenza che per i pari a 0 il ciclo non verrebbe più eseguito. 

Come per le variabili automatic, non è possibile conoscere il contenuto di una variabile register prima della sua esplicita inizializzazione mediante un operazione di assegnamento. In questo caso non si tratta di utilizzo e riutilizzo di un'area di memoria, ma di un registro macchina: non possiamo conoscerne a priori il contenuto nel momento in cui esso è destinato alla gestione della variabile (dichiarazione della variabile). Inoltre, analogamente alle variabili automatic, anche quelle register cessano di esistere all'uscita del blocco di codice (solitamente una funzione) nel quale sono dichiarate e il registro macchina viene utilizzato per altri scopi. 

Le variabili register, a differenza delle automatic, non hanno indirizzo: ciò appare ovvio se si pensa che i registri macchina si trovano nella CPU e non nella RAM. La conseguenza immediata è che una variabile register non può mai essere referenziata tramite un puntatore. Nel nostro esempio, il tentativo di assegnare ad un puntatore l'indirizzo di i provocherebbe accorate proteste da parte del compilatore. 

    register i;
    int *iPtr = &i;        // errore! i non ha indirizzo

Pur non avendo indirizzo, le variabili register possono contenere un indirizzo, cioè un puntatore: la dichiarazione 

    register char *ptr_1, char *ptr_2;

non solo è perfettamente lecita, ma anzi genera, se possibile, due puntatori (a carattere) particolarmente efficienti. 

Le variabili static 

Una variabile è static se dichiarata utilizzando, appunto, la parola chiave static

    static float sF, *sFptr;

Nell'esempio sono dichiarate due variabili static: una di tipo float e un puntatore (static anch'esso) ad un float

Come nel caso delle variabili automatic, quelle static sono locali al blocco di codice in cui sono dichiarate (e dunque sono accessibili solo all'interno di esso). La differenza consiste nel fatto che le variabili static hanno durata estesa a tutto il tempo di esecuzione del programma. Esse, pertanto, esistono già prima che il blocco in cui sono dichiarate sia eseguito e continuano ad esistere anche dopo il termine dell'esecuzione del medesimo. 

Ne segue che i valori in esse contenuti sono persistenti; quindi se il blocco di codice viene nuovamente eseguito esse si presentano con il valore posseduto al termine dell'esecuzione precedente. 

In altre parole, il compilatore alloca in modo permanente alle variabili static la memoria loro necessaria. 

Il tutto può essere chiarito con un paio di esempi: 

#include <stdio.h>

void incrementa(void)
{
    int x = 0;

    ++x;
    printf("%d\n",x);
}

void main(void)
{
    incrementa();
    incrementa();
    incrementa();
}

Il programma chiama la funzione incrementa() 3 volte; ad ogni chiamata la variabile x, automatic, è dichiarata ed inizializzata a 0. Essa è poi incrementata e visualizzata. L'output del programma è il seguente: 

1
1
1

Infatti x, essendo una variabile automatic, "sparisce" al termine dell'esecuzione della funzione in cui è dichiarata. Ad ogni chiamata essa è nuovamente allocata, inizializzata a 0, incrementata, visualizzata e... buttata alle ortiche. Indipendentemente dal numero di chiamate, incrementa() visualizza sempre il valore 1

Riprendiamo ora la funzione incrementa(), modificando però la dichiarazione di x

void incrementa(void)
{
    static int x = 0;

    ++x;
    printf("%d\n",x);
}

Questa volta x è dichiarata static. Vediamo l'output del programma: 

1
2
3

La x è inizializzata a 0 solo una volta, al momento della compilazione. Durante la prima chiamata ad incrementa(), essa assume pertanto valore 1. Poiché x è static, il suo valore è persistente e non viene perso in uscita dalla funzione. Ne deriva che alla seconda chiamata di incrementa() essa assume valore 2 e, infine, 3 alla terza chiamata. 

Quando si specifica un valore iniziale per una variabile automatic, detto valore è assegnato alla variabile ogni volta che viene eseguito il blocco in cui la variabile stessa è dichiarata. Una inizializzazione come: 

{
    int x = 1;
    ....

non è che una forma abbreviata della seguente: 

{
    int x;

    x = 1;
    ....

Quanto detto non è vero per le variabili static. Il valore iniziale di 1 nella seguente riga di codice: 

    static int x = 1;

viene assegnato alla variabile x una sola volta, in fase di compilazione: il compilatore riserva spazio per la variabile e vi memorizza il valore iniziale. Quando il programma è eseguito, il valore iniziale della variabile è già presente in essa. 

Se il programmatore non inizializza esplicitamente una variabile static, il compilatore le assegna automaticamente il valore NULL, cioè lo zero. 

Va poi sottolineato che l'accessibilità di una variabile static è comunque limitata (come per le varibili automatic) al blocco di codice in cui è dichiarata. Nel programma riportato per esempio, la variabile x non è accessibile né in main() né in qualunque altra funzione eventualmente definita, ma solamente all'interno di incrementa()

Infine, è opportuno ricordare che un array dichiarato in una funzione deve necessariamente essere dichiarato static se inizializzato contestualmente alla dichiarazione. 

Le variabili external 

Sono variabili external tutte quelle dichiarate al di fuori delle funzioni. Esse hanno durata estesa a tutto il tempo di esecuzione del programma, ed in ciò appaiono analoghe alle variabili static, ma differiscono da queste ultime in quanto la loro accessibilità è globale a tutto il codice del programma. In altre parole, è possibile leggere o modificare il contenuto di una variabile external in qualsiasi funzione. Vediamo, come sempre, un esempio: 

#include <stdio.h>

int x = 123;

void incrementa(void)
{
    ++x;
    printf("%d\n",x);
}

void main(void)
{
    printf("%d\n",x);
    incrementa();
    printf("%d\n",x);
}

L'output del programma è il seguente: 

123
124
124

Infatti la variabile x, essendo definita al di fuori di qualunque funzione, è accessibile sia in main() che in incrementa() e il suo valore è conservato per tutta la durata dell'esecuzione. 

Se una variabile external (o globale) ha nome identico a quello di una variabile automatic (o locale), quest'ultima "nasconde" la prima. Il codice che segue: 

#include <stdio.h>

int x = 123;

void main(void)
{
    printf("%d\n",x);
    {
        int x = 321;

        printf("%d\n",x);
    }
    printf("%d\n",x);
}

produce il seguente output: 

123
321
123

Infatti la x locale dichiarata nel blocco di codice interno a main() nasconde la x globale, dichiarata fuori dalla funzione; tuttavia la variabile locale cessa di esistere alla fine del blocco, pertanto quella globale è nuovamente accessibile. 

Anche le variabili external, come quelle static, sono inizializzate dal compilatore al momento della compilazione, ed è loro attribuito valore 0 se il programmatore non indica un valore iniziale contestualmente alla dichiarazione. 

Come abbiamo visto, le variabili external devono essere dichiarate al di fuori delle funzioni, senza necessità di specificare alcuna particolare parola chiave. Tuttavia, esse possono (ma non è obbligatorio) essere dichiarate anche all'interno delle funzioni che le referenziano, questa volta necessariamente precedute dalla parola chiave extern

#include <stdio.h>

int x = 123;

void main(void)
{
    extern int x;  // riga facoltativa; se c'e' non puo' reinizializzare x

    printf("%d\n",x);
}

In effetti il compilatore non richiede che le variabili external vengano dichiarate all'interno delle funzioni, ma in questo caso è necessario che tali variabili siano state dichiarate al di fuori della funzione e in linee di codice precedenti quelle della funzione stessa, come negli esempi precedenti. Se tali condizioni non sono rispettate il compilatore segnala un errore di simbolo non definito: 

#include <stdio.h>

int x = 123;

void main(void)
{
    printf("%d\n",x,y);   // errore! y non e' stata ancora dichiarata
}

int y = 321;

Il codice dell'esempio è compilato correttamente se si dichiara extern la y in main()

#include <stdio.h>

int x = 123;

void main(void)
{
    extern int x;  // facoltativa
    extern int y;  // obbligatoria!

    printf("%d\n",x,y);
}

int y = 321;

Il problema può essere evitato dichiarando tutte le variabili globali in testa al sorgente, ma se una variabile external e una funzione che la referenzia sono definite  in due file sorgenti diversi[27], è necessario comunque dichiarare la variabile nella funzione. 

E' opportuno limitare al massimo l'uso delle funzioni external: il loro utilizzo indiscriminato, infatti, può generare risultati catastrofici. In un programma qualsiasi è infatti piuttosto facile perdere traccia del significato delle variabili, soprattutto quando esse siano numerose. Inoltre le variabili globali sono generate al momento della compilazione ed esistono durante tutta l'esecuzione, incrementando così lo spazio occupato dal file eseguibile e la quantità memoria utilizzata dallo stesso. Infine, con esse non è possibile utilizzare nomi localmente significativi (cioè significativi per la funzione nella quale vengono di volta in volta utilizzate) e si perde la possibilità di mantenere ogni funzione una entità a se stante, indipendente da tutte le altre. 

Va infine osservato che una variabile external può essere anche static

#include <stdio.h>

static int x = 123;

void main(void)
{
    printf("%d\n",x);
}

Dichiarando static una variabile globale se ne limita la visibilità al solo file in cui essa è dichiarata: nel caso di un codice suddiviso in più sorgenti, le funzioni definite in altri file non saranno in grado di accedere alla x neppure qualora essa venga dichiarata con extern al loro interno. Naturalmente è ancora possibile dichiarare extern la variabile nelle funzioni definite nel medesimo file: 

#include <stdio.h>

static int x = 123;

void main(void)
{
    extern int x;

    printf("%d\n",x);
}

Come facilmente desumibile dall'esempio, la parola chiave static non deve essere ripetuta. 

Le costanti 

Le costanti, in senso lato, sono dati che il programma non può modificare. Una costante è, ad esempio, la sequenza di caratteri "Ciao Ciao!\n" vista in precedenza: per la precisione, si tratta di una costante stringa. Essa non può essere modificata perché non le è associato alcun nome simbolico a cui fare riferimento in un'operazione di assegnazione. Una costante è un valore esplicito, che può essere assegnato ad una variabile, ma al quale non può essere mai assegnato un valore diverso da quello iniziale. 

Ad esempio, una costante di tipo character (carattere) è un singolo carattere racchiuso tra apici. 

    char c1, c2 = 'A';
    c1 = 'b';
    c2 = c1;
    'c' = c2;      //ERRORE! impossibile assegnare un valore a una costante

Una costante intera con segno è un numero intero: 

    int unIntero = 245, interoNegativo = -44;

Una costante intera senza segno è un numero intero seguito dalla lettera U, maisucola o minuscola, come ci insegna il nostro CIAO2.C

    unsigned int anni = 31U;

Per esprimere una costante di tipo long occorre posporle la lettera Lmaiuscola o minuscola[28]

    long abitanti = 987553L;

Omettere la L non è un reato grave... il compilatore segnala con un warning che la costante è long e procede tranquillamente. In effetti, questo è l'atteggiamento tipico del compilatore C: quando qualcosa non è chiaro tenta di risolvere da sé l'ambiguità, e si limita a segnalare al programmatore di avere incontrato qualcosa di... poco convincente. Il compilatore C "presume" che il programmatore sappia quel che sta facendo e non si immischia nelle ambiguità logiche più di quanto sia strettamente indispensabile. 

Una U (o u) individua una costante unsigned; le costanti unsigned long sono identificate, ovviamente, da entrambe le lettere U e L, maiuscole o minuscole, in qualsivoglia ordine. Le costanti appartenenti ai tipi integral possono essere espresse sia in notazione decimale (come in tutti gli esempi visti finora), sia in notazione esadecimale (anteponendo i caratteri 0x o 0X al valore) sia in notazione ottale (anteponendo uno 0 al valore). 

    char beep = 07;        // ottale; 7
    unsigned long uLong = 12UL;    // decimale; 12 unsigned long
    unsigned maxUInt = 0xFFFFU;    // esadecimale; 65535 unsigned

Una costante di tipo floating point in doppia precisione (double) può essere espressa sia in notazione decimale che in notazione esponenziale: in questo caso si scrive la mantissa seguita dalla lettera E maiuscola o minuscola, a sua volta seguita dall'esponente. Per indicare che la costante è in singola precisione (float), occorre posporle la lettera F, maiuscola o minuscola. Per specificare una costante long double occorre la lettera L

    float varF = 1.0F;
    double varD = 1.0;
    double varD_2 = 1.;    // lo 0 dopo il punto decimale puo' essere omesso
    long double varLD = 1.0L;      // non e' un long int! C'e' il punto decimale!
    double varD_3 = 2.34E-2;       // 0.0234

Dagli esempi si deduce immediatamente che la virgola è espressa, secondo la convenzione anglosassone, con il punto ("."). 

Il C non riconosce le stringhe come tipo di dato, ma ammette l'utilizzo di costanti stringa (seppure con qualche limite, di cui si dirà): esse sono sequenze di caratteri racchiuse tra virgolette, come si è visto in più occasioni. Quanti byte occupa una stringa? Il numero dei caratteri che la compongono... più uno. In effetti le stringhe sono sempre chiuse da un byte avente valore  zero binario[29], detto terminatore di stringa. Il NULL finale è generato automaticamente dal compilatore, non deve essere specificato esplicitamente. 

Attenzione: le sequenze di caratteri particolari, come "\n", sono considerate un solo carattere (ed occupano un solo byte). I caratteri che non rientrano tra quelli presenti sulla tastiera possono essere rappresentati con una backslash (barra inversa) seguita da una "x" e dal codice ASCII esadecimale a due cifre del carattere stesso. Ad esempio, la stringa "\x07\x0D\x0A" contiene un "beep" (il carattere ASCII 7) e un ritorno a capo (i caratteri ASCII 13 e 10, questi ultimi equivalenti alla  sequenza ANSI "\n"[30]

I codici ASCII possono essere utilizzati anche per esprimere un singolo carattere: 

    char beep = '\x07';

E' del tutto equivalente assegnare ad una variabile char un valore decimale, ottale o esadecimale o, ancora, il valore espresso con \x tra apici. Attenzione, però: la rappresentazione ASCII di un carattere è cosa ben diversa dal suo valore ASCII; 7, 07, 0x07 e '\x07' sono tra loro equivalenti, ma diversi da '7'

La differenza tra un singolo carattere rispetto ad una stringa di un solo carattere sta negli apici, che sostistuiscono le virgolette. Inoltre, '\x07' occupa un solo byte, mentre "\x07" ne occupa due, uno per il carattere ASCII 7 e uno per il NULL che chiude ogni stringa. 

Non esistono costanti di tipo void

Le costanti manifeste 

Supponiamo di scrivere un programma per la gestione dei conti correnti bancari. E' noto (e se non lo era ve lo dico io) che nei calcoli finanziari la durata dell'anno è assunta pari a 360 giorni. Nel sorgente del programma si potrebbero perciò incontrare calcoli come il seguente: 

    interesse = importo * giorniDeposito * tassoUnitario / 360;

il quale impiega, quale divisore, la costante intera 360

E' verosimile che nel programma la costante 360 compaia più volte, in diversi contesti (principalmente in formule di calcolo finanziario). Se in futuro fosse necessario modificare il valore della costante (una nuova normativa legale potrebbe imporre di assumere la durata dell'anno finanziario pari a 365 giorni) dovremmo ricercare tutte le occorrenze della costante 360 ed effettuare la sostituzione con 365. In un sorgente di poche righe tutto ciò non rappresenterebbe certo un guaio, ma immaginando un codice di diverse migliaia di righe suddivise in un certo numero di file sorgenti, con qualche centinaio di occorrenze della costante, è facile prevedere quanto gravoso potrebbe rivelarsi il compito, e quanto grande sarebbe la possibilità di non riuscire a portarlo a termine senza errori. 

Il preprocessore C consente di aggirare l'ostacolo mediante la direttiva #define, che associa tra loro due sequenze di caratteri in modo tale che, prima della compilazione ed in modo del tutto automatico, ad ogni occorrenza della prima (detta manifest constant) è sostituita la seconda. 

Il nome della costante manifesta ha inizio col primo ­ carattere non blank[31] che segue la direttiva #define e termina con il carattere che precede il primo successivo non­spazio; tutto quanto segue quest'ultimo è considerato stringa di sostituzione. 

Complicato? Solo in apparenza... 

#define    GG_ANNO_FIN   360 //durata in giorni dell'anno finanziario

....

    interesse = importo * giorniDeposito * tassoUnitario / GG_ANNO_FIN;

L'esempio appena visto risolve il nostro problema: modificando la direttiva #define in modo che al posto del 360 compaia il 365 e ricompilando il programma, la sostituzione viene effettuata automaticamente in tutte le righe in cui compare GG_ANNO_FIN

Va sottolineato che la direttiva #define non crea una variabile, né è associata ad un tipo di dato particolare: essa informa semplicemente il preprocessore che la costante manifesta, ogniqualvolta compaia nel sorgente in fase di compilazione, deve essere rimpiazzata con la stringa di sostituzione. Gli esempi che seguono forniscono ulteriori chiarimenti: in essi sono definite costanti manifeste che rappresentano, rispettivamente, una costante stringa, una costante in virgola mobile, un carattere esadecimale e una costante long integer, ancora esadecimale. 

#define    NOME_PROG     "Conto 1.0"   //nome del programma
#define    PI_GRECO      3.14   //pi greco arrotondato
#define    RETURN        0x0D     //ritorno a capo
#define    VIDEO_ADDRESS 0xB8000000L       //indirizzo del buffer video

Le costanti manifeste possono essere definite utilizzando altre costanti manifeste, purché definite in precedenza: 

#define    N_PAG_VIDEO   8   //numero di pagine video disponibili
#define    DIM_PAG_VIDEO 4000      //4000 bytes in ogni pagina video
#define    VIDEO_MEMORY  (N_PAG_VIDEO * DIM_PAG_VIDEO)      //spazio memoria video

Una direttiva #define può essere suddivisa in più righe fisiche mediante l'uso della backslash: 

#define    VIDEO_MEMORY    \
           (N_PAG_VIDEO * DIM_PAG_VIDEO)

L'uso delle maiuscole nelle costanti manifeste non è obbligatorio; esso tuttavia è assai diffuso in quanto consente di individuarle più facilmente nella lettura dei sorgenti. 

Come tutte le direttive al preprocessore, anche la #define non si chiude mai con il punto e virgola (un eventuale punto e virgola verrebbe inesorabilmente considerato parte della stringa di sostituzione); inoltre il crosshatch ("#", cancelletto) deve trovarsi in prima colonna. 

La direttiva #define, implementando una vera e propria tecnica di sostituzione degli argomenti, consente di definire, quali costanti manifeste, vere e proprie formule, dette macro, indipendenti dai tipi di dato coinvolti: 

#define    min(a,b)    ((a < b) ? a : b)      // macro per il calcolo del minimo tra due

Come si vede, nella macro min(a,b) non è data alcuna indicazione circa il tipo di a e b: essa utilizza l'operatore ? :, che può essere  applicato ad ogni tipo di dato[32]. Il programmatore è perciò libero di utilizzarla in qualunque contesto. 

Le macro costituiscono dunque uno strumento molto potente, ma anche pericoloso: in primo luogo, la mancanza di controlli (da parte del compilatore) sui tipi di dato può impedire che siano segnalate incongruenze logiche di un certo rilievo (sommare le pere alle mele, come si dice...). In secondo luogo, le macro prestano il fianco ai cosiddetti side­effect, o effetti collaterali. Il C implementa un particolare operatore, detto di  autoincremento[33], che accresce di una unità il valore della variabile a cui è anteposto: se applicato a uno dei parametri coinvolti nella macro, esso viene applicato più volte al parametro, producendo risultati indesiderati: 

    int var1, var2;
    int minimo;

    ....

    minimo = min(++var1, var2);

La macrosotituzione effettuata dal preprocessore trasforma l'ultima riga dell'esempio nella seguente: 

    minimo = ((++var1 < var2) ? ++var1 : var2);

E' facile vedere che esso si limita a sostituire alla macro min la definizione data con la #define, sostituendo altresì i parametri a e b con i simboli utilizzati al loro posto nella riga di codice, cioè ++var1 e var2. In tal modo var1 è incrementata due volte se dopo il primo incremento essa risulta ancora minore di var2, una sola volta nel caso opposto. Se min() fosse una funzione il problema non potrebbe verificarsi (una chiamata a funzione non è una semplice sostituzione di stringhe, ma un'operazione tradotta in linguaggio macchina dal compilatore seguendo precise regole); tuttavia una funzione non accetterebbe indifferentemente argomenti di vario tipo, e occorrerebbe definire funzioni diverse per effettuare confronti, di volta in volta, tra integer, tra floating point, e così via. Un altro esempio di effetto collaterale riguarda più da vicino il comportamento del compilatore

Diamo un'occhiata all'esempio che segue: 

#define  PROG_NAME    "PROVA"
....
    printf(PROG_NAME);
....
#undef  PROG_NAME
....
    printf(PROG_NAME);     // Errore! PROG_NAME non esiste piu'...

Quando una definizione generata con una #define non serve più, la si può annullare con la direttiva #undefine. Ogni riferimento alla definizione annullata, successivamente inserito nel programma, dà luogo ad una segnalazione di errore da parte del compilatore. 

Da notare che PROG_NAME è passata a printf() senza porla tra virgolette, in quanto esse sono già parte della stringa di sostituzione, come si può vedere nell'esempio. Se si fossero utilizzate le virgolette, printf() avrebbe scritto PROG_NAME e non PROVA: il preprocessore, infatti, ignora tutto quanto è racchiuso tra virgolette o apici. In altre parole, esso non ficca il naso nelle costanti stringa e in quelle di tipo carattere. 

Vale la pena di citare anche la direttiva #ifdef...#else...#endif, che consente di includere o escludere dalla compilazione un parte di codice, a seconda che sia, o meno, definita una determinata costante manifesta: 

#define DEBUG
....
#ifdef DEBUG
....    // questa parte del sorgente e' compilata
#else
....    // questa no (lo sarebbe se NON fosse definita DEBUG)
#endif

La direttiva #ifndef e' analoga alla #ifdef, ma lavora con logica inversa: 

#define DEBUG
....
#ifndef DEBUG
....    // questa parte del sorgente NON e' compilata
#else
....    // questa si (NON lo sarebbe se NON fosse definita DEBUG)
#endif

Le direttive #ifdef e #ifndef risultano particolarmente utili per scrivere codice portabile: le parti di sorgente differenti in dipendenza dal compilatore, dal sistema o dalla macchina possono essere escluse o incluse nella compilazione con la semplice definizione di una costante manifesta in testa al sorgente. 

Le costanti simboliche 

E' di recente diffusione, tra i programmatori C, la tendenza a limitare quanto più possibile l'uso delle costanti manifeste, in parte proprio per evitare la possibilità di effetti collaterali, ma anche per considerazioni relative alla logica della programmazione: le costanti manifeste creano problemi in fase di  debugging[34], poiché non è possibile sapere dove esse si trovino nella memoria dell'elaboratore (come tutte le costanti, non hanno indirizzo conoscibile); inoltre non sempre è possibile distinguere a prima vista una costante manifesta da una variabile, se non rintracciando la #define (l'uso delle maiuscole e minuscole è libero tanto nelle costanti manifeste quanto nei nomi di variabili, pertanto nulla garantisce che un simbolo espresso interamente con caratteri maiuscoli sia effettivamente una costante manifesta). 

Il C consente di definire delle costanti simboliche dichiarandole come vere e proprie variabili, ma anteponendo al dichiaratore di tipo la parola chiave const. Ecco un paio di esempi: 

    const int  ggAnnoFin = 360;
    const char return = 0x0D;

E' facile vedere che si tratta di dichiarazioni del tutto analoghe a quelle di variabili; tuttavia la presenza di const forza il compilatore a considerare costante il valore contenuto nell'area di memoria associata al nome simbolico. Il compilatore segnala come illegale qualsiasi tentativo di modificare il valore di una costante, pertanto ogni costante dichiarata mediante const deve essere inizializzata contestualmente alla dichiarazione stessa. 

    const int unIntCostante = 14;

    ....

    unIntCostante = 26;    //errore: non si puo' modificare il valore di una costante

Il principale vantaggio offerto da const è che risulta possibile accedere (in sola lettura) al valore delle costanti così dichiarate mediante l'indirizzo delle medesime (come accade per tutte le aree di memoria associate a nomi simbolici): ancora una volta rimandiamo gli approfondimenti alla trattazione dei puntatori

Infine, le costanti simboliche possono essere gestite dai debugger proprio come se fossero variabili. 

Entità complesse 

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[35], ma è comunque un passo avanti... 

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 <stdio.h>

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\n",c0.serie);
    printf("Organizzatore della concorso 1: %c\n",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 <stdio.h>

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\n",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\n",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 <stdio.h>

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\n",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\n",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\n",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. 

Le unioni 

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[43]. 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 {
    unsigned offset;
    unsigned segment;
};

union Far_c_Ptr {
    char far *ptr;
    struct FarPtrWords words;
};

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[44]

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 e non riguarda in modo diretto l'argomento "union": 

    union FarPtr fp;
    
    fp.ptr = (char far *)0xB8000000L;
    printf("ptr: %Fp\n",fp.ptr);
    printf("ptr: %X:%X\n",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 {
    char far *ptr;
    struct FarPtrWords words;
    unsigned pOffset;
}

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 

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 ai loro differenti modi di manifestarsi (ad esempio: al colore nero può essere associato il valore zero, al rosso il valore 1, 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. 

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 {
    ignoto,        // beh, non si sa mai...
    maschile,
    femminile
};

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 0, al secondo 1, 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 {
    ignoto = -1,
    maschile,
    femminile
};

Nell'esempio, al nome ignoto è assegnato esplicitamente valore ­1: il compilatore assegna valore 0 al nome maschile e 1 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

I campi di bit 

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 {
    unsigned sesso: 1;
    unsigned stato: 2;
    unsigned cura: 4;
    unsigned letto: 3;
};

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 0 e ­1

I campi di bit sono referenziabili esattamente come i campi di una comune struttura: 

enum SEX {
    maschile,
    femminile
};

struct CartellaClinica {
    unsigned sesso: 1;
    unsigned stato: 2;
    unsigned cura: 4;
    unsigned letto: 3;
};

char *sessi[] = {
    "MASCHILE",
    "FEMMINILE"
};
....
    struct CartellaClinica Paziente;
    ....
    Paziente.sesso = maschile;
    ....
    printf("Sesso del paziente: %s\n",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

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 {
    unsigned sesso: 1;
    unsigned stato: 2;
    unsigned cura: 4;
    unsigned letto: 3;
    unsigned ricoveri: 6;
};

Nella nuova definizione, tutti i 16 bit delle due word occupate in memoria dalla struct CartellaClinica sono utilizzati. 
OK, andiamo avanti a leggere il libro... 

Non ci ho capito niente! Ricominciamo...