coordinamento di Francesco Sileno
di Stefano Casini
[ Introduzione ]
[ La variabile ||
Interagire direttamente con la memoria ||
Il puntatore ||
Operazioni con i puntatori ||
Puntatori e Array ]
[ Conclusioni ]
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.
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:
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:
Abbiamo detto che anche il puntatore è una variabile, per cui non
deve sembrare strano volerne conoscere l'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).
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.
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.
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:
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
}
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
}
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
}
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
}
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
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 || >> ]