- Programmazione » Programmazione » Guida C - Manuale programmazione con articoli e risorse interessanti
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
AF 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...
- Quale significato ha l'espressione *&numPtr?
- Quale significato ha l'espressione **numPtr?
- E l'espressione *numero?
- E l'espressione &*numPtr?
- &*numPtr e numPtr sono la stessa cosa?
- Cosa restituisce l'espressione &&numero?
- E l'espressione &&numPtr?
- Cosa accade se si esegue *numPtr = 21.75?
- Cosa accade se si esegue numPtr = 0x24A6?
- E se si esegue &numPtr = 0xAF2B?
Le soluzione alla fine della guida.
- Articolo precedente I puntatori: gli indirizzi di memoria
- Articolo successivo I puntatori: complicazioni