Barninga Z
a- a+

Gli operatori * e &

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 = № // 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 " ,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 " ,*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 = №

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 " ,numero);
    printf("numero = %f " ,*numPtr);
    printf("l'indirizzo di numero e' %X " ,&numero);
    printf("l'indirizzo di numero e' %X " ,numPtr);
    printf("l'indirizzo di numPtr e' %X " ,&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

 

Le soluzione alla fine della guida.



Ti potrebbe interessare anche

commenta la notizia

C'è 1 commento
Lorenzo
Hai qualche domanda da fare?