Beta [ << || Pagina Principale || Sommario || Redazione || Informazioni || Beta Browser || >> ]

[Rubrica Programmazione]

coordinamento di Francesco Sileno



Chi ha paura dei puntatori?

di Stefano Casini


[ Introduzione ]

[ La variabile || Interagire direttamente con la memoria || Il puntatore || Operazioni con i puntatori || Puntatori e Array ]

[ Conclusioni ]


Introduzione

Chiunque si sia trovato a frequentare un corso di programmazione in linguaggio C è venuto a trovarsi, dopo qualche lezione, di fronte a un bivio: perdere il sonno nel tentativo di capire come si utilizzano i puntatori, o continuare a studiare come se questi non esistessero. Quando poi si inizia a programmare, la scarsa confidenza che questi misteriosi oggetti generano nella maggior parte degli esseri umani porta ad una selezione naturale: da una parte coloro, generalmente additati come pazzi o esibizionisti, che sanno riconoscere a prima vista un puntatore ad una funzione che accetta come parametro un puntatore a caratteri e ritorna un puntatore ad interi:

int * (* funcpointer)(char * varname);

da una funzione che accetta come parametro il puntatore a funzione che ho appena definito e ritorna un puntatore a puntatore ad interi:

int ** funcname(int * (* funcpointer)(char * varname));

dall'altra quelli che ancora non si rendono bene conto del perchè una stringa, in C, si dichiara come un puntatore a caratteri. Più si va avanti nel tempo, più questa forbice si allarga: i "bravi" si mettono le mani nei capelli perchè trovano il codice scritto dagli altri estremamente ridondante, i "somari" i capelli se li strappano perchè trovano intraducibile il codice scritto con interminabili sequenze di frecce e stellette; immaginate dentro un'azienda gli scompensi che questo diverso modo di programmare può portare nel caso di progetti di una certa importanza, dove il codice viene scritto da più persone e poi assiemato (sarà forse per questo motivo che gli illuminati imprenditori italiani seguono ancora la vecchia logica "un progetto- un uomo"?).

Con questo articolo (e, se troverete interessante quello che scrivo, con i prossimi) cercherò di spiegare in maniera semplice quelli che sono i concetti di non facile comprensione della programmazione in C, con particolare attenzione allo sviluppo di applicazioni per Windows versione 3.1 e successive.


La variabile

Per definizione, "la variabile è l' astrazione di una locazione di memoria": questa definizione fareste bene a scolpirla con lettere di fuoco nelle vostre locazioni di memoria così da non dimenticarla mai.
Ora che l'avete scolpita, vi spiego cosa significa: il file eseguibile costruito dal vostro compilatore è in grado di "vedere" la mappa della memoria, ovvero un certo quantitativo di celle di memoria, siano esse fisicamente presenti come RAM, siano esse presenti grazie a quel meccanismo software che si chiama "memoria virtuale"; ognuna di queste celle di memoria ha la dimensione di un byte, ed è individuata, rispetto alle altre celle, da un numero (indirizzo) che ne caratterizza la posizione nella mappa di memoria. Ogni tipo di dato elementare definito dal linguaggio C ha una certa dimensione in byte. Per esempio:

Il linguaggio C permette al programmatore di definire, con lo statement typedef, i propri tipi di dato a partire dai tipi di dato elementari. Ogni volta che, nel nostro codice, definiamo una variabile, il compilatore riserva nella mappa di memoria un numero di celle pari alla dimensione del tipo di dato assegnato alla variabile, e questo insieme di celle è la locazione di memoria associata alla variabile; il compilatore conserva internamente l'indirizzo della locazione di memoria per evitare di assegnare ad altre variabili le stesse celle che ha appena riservato per noi. A questo punto possiamo leggere e scrivere nella locazione di memoria associata alla nostra variabile senza avere la minima idea di quale sia la sua posizione nella mappa della memoria: sarà il compilatore a tradurre le nostre operazioni sulla variabile in operazioni di lettura e scrittura nelle celle di memoria della sua locazione; da qui discende il concetto espresso all'inizio: operazioni su una locazione di memoria fatte attravero operazioni astratte sul nome che abbiamo assegnato alla variabile.

Vediamo un esempio:


long Esempio1(void)
{
     long miaVar;
     // definizione di una variabile di tipo                     
     // long di nome miaVar: il compilatore alloca 4 byte      
     // in memoria e conserva l'indirizzo della locazione

     miaVar = 5;
     // assegniamo un valore a miaVar: il compilatore  
     // dal nome della variabile risale
     // all'indirizzo della locazione di memoria e vi  
     // scrive il valore 5

     return miaVar;
     // leggiamo il valore di miaVar: il compilatore   
     // dal nome della variabile risale
     // all'indirizzo della locazione di memoria e ne  
     // legge il contenuto
}

A questo punto qualcuno potrebbe chiedersi quand' è che la locazione di memoria torna ad essere disponibile per altre variabili, ovvero quando la variabile viene deallocata; nell'esempio fatto poc'anzi il compilatore si "dimentica" dell'indirizzo della locazione di memoria assegnata alla variabile non appena il ciclo delle istruzioni esce dall'intervallo di visibilità (scope) della variabile in oggetto, ovvero dopo la parentesi graffa di chiusura del corpo della funzione Esempio1. Questo meccanismo di allocazione e deallocazione automatica della locazione di memoria associata alla variabile, eseguito dal compilatore in maniera del tutto trasparente per il programmatore, viene chiamato allocazione statica di memoria: il compilatore è anche così intelligente da gestire in proprio variabili con lo stesso nome, definite in scope diversi, come nell'esempio che segue:

long Esempio2(void)
{
     long miaVar = 100;
     long Conta = 0;
     // le variabili miaVar e Conta sono visibili
     // all'interno di tutta la funzione

     for(Conta = 0; Conta < 5; Conta++)
     {
          long miaVar;
          long altraVar = 3;
          // le variabili miaVar e altraVar sono visibili        
          // solo all'interno del ciclo for; la
          // variabile miaVar definita in questo ciclo non       
          // è la stessa definita all'inizio della          
          // funzione:
          // all'interno di questo ciclo for la variabile        
          // miaVar interna al ciclo impedisce di vedere       
          // la miaVar esterna della funzione

          miaVar = altraVar * Conta;
     }

     if(Conta == 5)
     {
          long altraVar;
          // la variabile altraVar definita in questo       
          // ciclo non è la stessa definita nel ciclo for;
          // invece la variabile miaVar è quella visibile        
          // da tutta la funzione

          altraVar = miaVar * Conta;
          miaVar = miaVar + altraVar;
     }

     return miaVar;
     // quanto vale ora miaVar? 600.
}


Interagire direttamente con la memoria

Abbiamo visto come il compilatore sia in grado di gestire l'allocazione statica di memoria per le variabili che di volta in volta definiamo nel nostro codice, in maniera del tutto trasparente per il programmatore. Se però volessimo interagire in maniera più diretta con la mappa della memoria c'è la possibilità, offerta dal linguaggio C, di gestire in proprio il contenuto delle locazioni di memoria: per fare questo dobbiamo chiedere al compilatore di fornirci cortesemente l'indirizzo delle locazioni di memoria che ha allocato per le nostre variabili; dopodichè, potremo leggere e scrivere direttamente nella mappa di memoria, senza passare attraverso la fase di traduzione nome variabile-indirizzo della locazione, che rallenta l'esecuzione del programma. Naturalmente, interagire direttamente con la memoria non è tutto rose e fiori: è facile, sopratutto le prime volte, andare a scrivere negli indirizzi sbagliati o addirittura nelle zone protette del sistema operativo (laddove ve lo fa fare, ndr), con effetti catastrofici (avete mai visto comparire il messaggio General Protection Fault, con Windows che si blocca e la necessità di resettare il computer? Ecco); comunque i vantaggi legati all'uso dei puntatori, secondo me, superano di gran lunga gli svantaggi, per cui vale senz'altro la pena di studiarli, capirli ed utilizzarli.


Il puntatore

Possiamo definirlo così: "il puntatore è una variabile il cui valore è l'indirizzo di una locazione di memoria": però attenzione, questo valore numerico ha significato solo all'interno dello scope di visibilità della variabile associata; usciti dallo scope della variabile, questa viene deallocata dal compilatore, per cui la locazione di memoria associata in precedenza può essere messa a disposizione di altre variabili; in pratica, è come andare all'indirizzo di una bella ragazza che però ha traslocato, con il rischio di trovare nel suo appartamento un camionista per nulla attraente.

void Esempio3(void)
{
     int flag = 1;
     long valore;
     long * puntatore;
     // la variabile puntatore è visibile in tutta la funzione

     if(flag == 1)
     {
          long miaVar = 5;
          // la variabile miaVar è visibile solo dentro l'if

          puntatore = & miaVar;
          // assegniamo alla variabile puntatore       
          // l'indirizzo di miaVar

          valore = * puntatore;
          // leggiamo il valore della variabile
          // miaVar, puntata dalla variabile puntatore
     }

     valore = * puntatore;
     // questa operazione non ha senso, perchè la
     // variabile miaVar non è più allocata, e nella   
     // locazione puntata dalla variabile puntatore potrà
     // esserci chissà cosa
}

Abbiamo detto che anche il puntatore è una variabile, per cui non deve sembrare strano volerne conoscere l'indirizzo:

void Esempio4(void)
{
     long miaVar = 5;
     long * puntatore;
     long ** puntApunt;
     long * Indirizzo;
     long Valore;

     puntatore = & miaVar;
     puntApunt = & puntatore;

     Indirizzo = * puntApunt;
     // la variabile Indirizzo ora contiene l'indirizzo     
     // di miaVar

     Valore = * Indirizzo;
     // la variabile Valore ora contiene il valore di miaVar

     Valore = ** puntApunt;
     // è equivalente a scrivere Valore = * Indirizzo
}

Come nelle migliori cacce al tesoro, possiamo divertirci a seminare puntatori; con un pò di pazienza, potremo ritrovare la bella ragazza di cui sopra partendo da Piazza Venezia, dove troveremo una busta con un indirizzo di via Cavour, dove troveremo una busta con un indirizzo di via Merulana, dove troveremo una busta con un indirizzo di via Appia, dove troveremo una busta con un indirizzo di via Casilina, dove finalmente troveremo la nostra amata (che, guardacaso, aveva traslocato proprio nell'appartamento di fronte al nostro).

void Esempio5(BELLARAGAZZA ***** piazzaVenezia)
{
     // BELLARAGAZZA è un tipo di dato da noi definito
     // con il typedef

     BELLARAGAZZA **** viaCavour;
     BELLARAGAZZA *** viaMerulana;
     BELLARAGAZZA ** viaAppia;
     BELLARAGAZZA * viaCasilina;
     BELLARAGAZZA AnnaFalchi;

     viaCavour = * piazzaVenezia;
     // l'indirizzo di via Cavour

     viaMerulana = * viaCavour;
     // l'indirizzo di via Merulana

     viaAppia = * viaMerulana;
     // l'indirizzo di via Appia

     viaCasilina = * viaAppia;
     // l'indirizzo di via Casilina

     AnnaFalchi = * viaCasilina;
     // è stata dura, ma ne valeva la pena!
}


Operazioni con i puntatori

Riassumiamo brevemente gli operatori del linguaggio C che hanno a che fare con i puntatori:

 &   è l'operatore di indirizzo;
*  è l'operatore di indirezione;
-> è l'operatore di indirezione del membro di una struttura.

Sia miaVar la nostra variabile, che rappresenti un tipo di dato semplice (int, char, etc.) o una struttura (struct), e mioPunt una variabile che ne rappresenti l'indirizzo della locazione di memoria associata: per prima cosa, onde evitare messaggi d'errore durante la compilazione del codice, dobbiamo dichiarare la variabile mioPunt in modo congruente con la variabile di cui rappresenterà l'indirizzo; perciò, se miaVar è un intero, mioPunt sarà un puntatore ad intero; se miaVar è una struttura di tipo PUNTO, mioPunt sarà un puntatore alla struttura PUNTO.
L'assegnazione a mioPunt dell'indirizzo di miaVar si esegue con l'operatore & applicato a miaVar. Se invece, noto l'indirizzo mioPunt della variabile, volessimo risalire al contenuto di miaVar, dovremo applicare l'operatore di indirezione * a mioPunt: inoltre, nel caso miaVar fosse una struttura di tipo PUNTO e fossimo interessati ai valori dei singoli membri della struttura, potremo usare direttamente l'operatore ->.

void Esempio6(void)
{
     struct PUNTO{int x; int y;} miaVar = {3, 9};
     // dichiaro una struttura di tipo PUNTO, e la
     // inizializzo
     struct PUNTO * mioPunt;
     // dichiaro il puntatore ad una struttura di tipo      
     // PUNTO
     int xCoord, yCoord;

     mioPunt = &miaVar;
     // assegno al puntatore l'indirizzo della struttura

     miaVar = * mioPunt;
     // ricavo il contenuto della struttura a partire dal   
     // suo indirizzo

     xCoord = mioPunt->x;
     yCoord = mioPunt->y;
     // ricavo il contenuto dei singoli membri della   
     // struttura a partire dal suo indirizzo: è
     // equivalente a scrivere

     xCoord = (* mioPunt).x;
     yCoord = (* mioPunt).y;
}


Puntatori e array

Nel linguaggio C è possibile eseguire lo operazioni di indirizzo e di indirezione agli array in maniera del tutto particolare: infatti, quando dichiariamo un array, il compilatore assegna automaticamente come contenuto della variabile che porta il nome dell'array l'indirizzo della locazione di memoria del primo elemento dell'array; pertanto il nome dell'array, privato delle parentesi quadre, ne rappresenta l'indirizzo. Quando vogliamo eseguire l'operazione di indirezione su di un puntatore ad un array, l'uso dell'operatore * ci fornisce solo il contenuto del primo elemento dell'array; per accedere agli elementi successivi, basta applicare le parentesi quadre con l'indice dell'elemento desiderato al nostro puntatore, senza preporre l'operatore di indirezione.

void Esempio7(void)
{
     int mioArray[] = {3, 9, 12, 34, 21};
     // dichiaro un array di 5 elementi interi e lo
     // inizializzo
     int * mioPunt;
     // dichiaro un puntatore ad interi
     int valore;

     mioPunt = mioArray;
     // assegno al puntatore l'indirizzo dell'array
     // è equivalente a scrivere

     mioPunt = &mioArray[0];

     valore = * mioPunt;
     // ricavo solo il contenuto del primo elemento
     // dell'array: è equivalente a scrivere

     valore = mioPunt[0];

     valore = mioPunt[2];
     // ricavo il contenuto del terzo elemento dell'array
}

Purtroppo, l'impossibilità di conoscere la dimensione di un array (o di una struttura) tramite la semplice operazione di indirezione del suo puntatore costituisce un limite del linguaggio ed anche una continua fonte di errori di programmazione: l'applicazione dell'operatore sizeof ad un puntatore a qualsiasi tipo di struttura o array restituisce sempre il medesimo valore, la dimensione in byte della variabile puntatore, a prescindere dall'effettiva dimensione della variabile puntata.

void Esempio8(void)
{
     long tipo3[5];
     struct PUNTO {int x; int y;} tipo4;
     long * punt3;
     int size;

     punt3 = tipo3;

     size = sizeof(char);            // 1
     size = sizeof(int);             // 2
     size = sizeof(tipo3);           // 20
     size = sizeof(struct PUNTO);    // 4
     size = sizeof(char *);          // 2
     size = sizeof(int *);           // 2
     size = sizeof(punt3);           // 2
     size = sizeof(struct PUNTO *);  // 2
}


Conclusioni

Per chiudere questo breve discorso sui puntatori vorrei riassumere alcuni concetti:

La prossima volta vi parlerò sia del passaggio di parametri alle funzioni per indirizzo, sia dell'allocazione dinamica di memoria, che non può prescindere dall'utilizzo di questi maledetti (da alcuni, benedetti da altri) bacarozzi a forma di asterisco.



Copyright
© 1996 Beta Working Group. Tutti i diritti riservati
Conversione e impaginazione HTML a cura di Davide Rossi


Beta [ << || Pagina Principale || Sommario || Redazione || Informazioni || Beta Browser || >> ]