Puntatori
Un puntatore è un oggetto il cui valore rappresenta l'indirizzo di un
altro oggetto o di una funzione.
Nelle seguenti dichiarazioni p e q sono puntatori ad interi..
int *p,*q;
In linguaggio C per ottenere l'indirizzo di un oggetto si usa l'operatore
& il cui risultato può essere assegnato ad un puntatore.
Per accedere all'oggetto riferito da un puntatore si usa l'operatore *
.
int i=0, j=0;
int *p, *q;
p=&i; //p = indirizzo di i
*p=3; //equivale a i=3
j=*p; //equivale a j=i
q=p; //equivale a q=&i
nell'esempio precedente i e j
sono due variabili intere p e q
sono due variabili destinate a contenere gli indirizzi di variabili intere.
Stato di un puntatore
Con un puntatore è possibile, in teoria, raggiungere via software ognuna
delle celle di memoria esistenti sul computer ospitante, ma in pratica non
è saggio farlo (e con i Sistemi Operativi moderni non è nemmeno possibile),
poiché in taluni casi si rischia un crash di sistema.
Tentare di accedere al di fuori dello spazio di indirizzi riservato al programma
genera una violazione, che il sistema operativo ci segnala con un messaggio
di errore e con l'interruzione forzata del codice che stiamo eseguendo.
Nel servirci dei puntatori dobbiamo essere sempre sicuri di dove
stiamo puntando.
Un puntatore può trovarsi in uno dei tre seguenti stati:
Riferito correttamente: significa
che contiene l'indirizzo dell'entità a cui intendiamo puntare, situata solitamente
all'interno del programma, oppure in memoria dinamica.
Riferito non correttamente : significa
che contiene un indirizzo casuale oppure non significativo, magari pure
vietato dal Sistema Operativo. Ad esempio: - un indirizzo casuale, derivante
da una mancata inizializzazione del puntatore;
● L'indirizzo di una zona di memoria riservata
a un altro programma;
● L'indirizzo di una variabile precedentemente
esistita su stack (una procedura chiamante Proc1 non può accedere in alcun
modo alle variabili locali della procedura richiamata Proc2, poiché queste,
allocate su stack, cessano di esistere nel momento stesso in cui Proc2 termina
e torna al chiamante);
● L'indirizzo di un blocco di memoria dinamica
precedentemente esistito e poi successivamente rilasciato (restituito al
Sistema Operativo);
● L'indirizzo di una cella di memoria non
esistente sul computer ospitante. Graficamente, la situazione si rappresenta
così:
Annullato : significa che il puntatore "punta da nessuna parte", ossia "non punta". A tale proposito, al puntatore deve essere assegnato un valore particolare, predefinito dal linguaggio con una costante apposita (in C tale costante si chiama NULL, in Pascal si chiama NIL). Numericamente, sui computer basati su processori 80x86, tale costante equivale al valore zero, codificato su 32 bit. Graficamente, il puntatore viene rappresentato come se fosse puntato "a massa":
Inizializzazione dei puntatori (regola importante)
Per evitare problemi, è buona regola della programmazione (ma non è un
obbligo) annullare (o inizializzare) i puntatori nel momento in cui vengono
dichiarati:
char * Punt = NULL;
/* Dichiara un puntatore a
carattere,(Punt) e lo annulla */
int V=25; int * PV = &V;
/* Puntatore a intero, immediatamente inizializzato
puntandolo su V, già esistente */
float * Killer; /* Dichiara un puntatore a float.
In questo momento non è correttamente riferito */
Coerenza dei puntatori
Il linguaggio C è assai rigoroso nella gestione dei puntatori: puntamenti,
accessi indiretti e passaggi di parametri per indirizzo avvengono correttamente
solo se vi è coerenza fra puntatore ed entità puntata:
● per puntare a una variabile intera è necessario
utilizzare un puntatore a intero;
● per puntare a una variabile float è necessario
utilizzare un puntatore a float;
● per puntare a una variabile char è necessario
utilizzare un puntatore a carattere;
● per puntare a una variabile record è necessario
utilizzare un puntatore a struttura del medesimo tipo… - ecc.
Un puntatore dichiarato specificamente per entità di un certo tipo si definisce
puntatore tipizzato o coerente.
Anche se due indirizzi hanno fisicamente il medesimo formato (stesso numero
di bit, stessa struttura Segmento:Offset), non è lecito assegnare a un puntatore
tipizzato l'indirizzo contenuto in un puntatore tipizzato diversamente.
Ad esempio a un puntatore a intero non si può assegnare l'indirizzo contenuto
in un puntatore a carattere:
puntatore a intero = puntatore a carattere;
/* Svolta in questo modo, è un'istruzione illecita
*/
Puntatore generico e conversione di puntatori
Esistono casi in cui non è molto chiaro come ottenere la coerenza, e quindi
come dichiarare il puntatore:
● se vogliamo puntare a una procedura/funzione,
che tipo di puntatore dobbiamo utilizzare?
● se chiediamo al Sistema Operativo un blocco
di memoria dinamica di N bytes, come facciamo ad accedervi, dato che il
tipo "byte" non esiste in C, e quindi nemmeno possiamo costruire il tipo
"vettore di N byte"?
In questi casi, dobbiamo servirci di un puntatore
generico o non tipizzato,
dichiarato come segue:
void *Punt = NULL; /* Puntatore generico,
annullato */
Il puntatore generico è caratterizzato
dal fatto che può contenere qualsiasi indirizzo, ed è compatibile con tutti
i tipi di puntatori, pertanto l'assegnazione seguente è perfettamente
lecita:
puntatore generico = puntatore tipizzato di qualsiasi tipo;
L'assegnazione contraria, invece, è illecita. Disponiamo però di uno strumento
che ci permette di aggirare l'ostacolo: la forzatura del puntatore generico,
tipizzandolo tramite un casting.
Ammesso che abbiano senso, le forzature si effettuano nei modi seguenti:
puntatore tipizzato= (tipo *) puntatore generico;
/* Es: P_int=(int *)P_void; */
puntatore tipizzato tipo1 =(tipo1 *) puntatore tipizzato tipo2;
/* Es: P_float = (float *) P_char; */
Alcuni esempi specifici sono riportati in seguito.
Operatori per i puntatori
Utilizzando i puntatori, è necessario familiarizzare con i seguenti operatori:
& operatore di puntamento
serve per generare l'indirizzo di una entità (&entità)
* operatore di indirizzamento
serve per accedere all'entità tramite il puntatore
(*puntatore)
-> operatore "freccia"
serve per accedere ai campi di un record tramite puntatore
** operatore di doppio indirizzamento
serve per la doppia indirezione (**puntatore a puntatore)
Dichiarazione di puntatori - esempi di accesso indiretto
Per dichiarare un puntatore tipizzato:
tipo *nome; /* Puntatore non correttamente
riferito */
tipo *nome = NULL; /* Puntatore annullato */
tipo *nome = &entità indirizzabile; /* Puntatore riferito
a un'entità dichiarata PRIMA del puntatore stesso */
Per dichiarare un puntatore generico, ossia "di nessun tipo", si utilizza
la parola chiave void:
Esempio 1 - Dichiarazione
di puntatori
int *Punt1 = NULL; /* Puntatore a
intero, annullato */
char *Punt2; /* Puntatore a carattere, non correttamente
riferito */
void *Punt3; /* Puntatore generico, non correttamente
riferito */
Esempio 2 - Accesso indiretto
Abbiamo bisogno anche di entità indirizzabili, altrimenti i puntatori dove
li facciamo puntare? A cosa accediamo?
float Area, Raggio = 153.12; /* "Raggio"
sarà l'entità indirizzabile */
float *Punt4 = &Raggio; /* Puntatore a float, puntato
sulla variabile Raggio */
…
Area = Raggio * Raggio * 3.14; /* Espressione di esempio,
con 2 accessi diretti alla variabile Raggio */
Area = (*Punt4) * (*Punt4) * 3.14; /* Stessa espressione,
effettuata con due accessi indiretti a Raggio */
Dato che "Punt4" punta a "Raggio", l'entità "*Punt4" è la variabile "Raggio",
ma espressa in modo indiretto.
Esempio 3 - Altri accessi
indiretti
int V = 1;
int * PV; /* Puntatore a intero, non riferito */
…
PV = &V; /* Punta a V (non è obbligatorio inizializzare
PV nella dichiarazione) */
*PV = 0; /* Azzera V, con un accesso indiretto in
scrittura (V viene modificata)*/
(*PV)++; /* Incrementa V, tramite un accesso indiretto
in scrittura */
Esempio 4 - Assegnazione fra
puntatori tipizzati diversamente
char Yes = 'S';
int *P1;
char *P2 = &Yes;
…
P1 = (int *) P2
/* Copia P2 in P1: l'indirizzo contenuto in P2 viene
trasformato in un
indirizzo di entità intera, e quindi assegnato a un puntatore a intero.
Tale operazione normalmente ha poco senso: per accedere a Yes si
utilizzerà comunque un puntatore a carattere, e quindi P2. */
Esempio 5 - Utilizzo di un
puntatore generico per accedere a entità di tipo specifico
float F;
void *P; /* Puntatore generico: potrà puntare a qualsiasi
entità */
…
P = &F ;
/* Nessun problema: P è compatibile con indirizzi di qualsiasi entità */
*(float *)P = 0.0;
/* Azzera F indirettamente, convertendo "al volo"
P in punt. a float */
Esempio 6 - Doppia indirezione
int V;
int *PV = &V; /* Punta alla variabile intera V */
int ** PPV = &PV; /* Punta al puntatore a intero PV
*/
…
*PV = 0; /* Azzera indirettamente V */
**PPV = 0; /* Azzera V mediante una doppia indirezione
*/
Esempio 7 - Elisione reciproca
degli operatori di puntamento e di indirizzamento
Dato che il puntamento e l'indirizzamento sono complementari, talvolta si
possono elidere a vicenda:
int V;
int * PV = &V;
…
scanf ("%d", &V); /* Acquisiamo V riferendoci direttamente
ad essa, per indirizzo */
scanf ("%d", &(*PV));
/* Acquisiamo V indirettamente (sostituiamo *PV al
posto di V) */
scanf ("%d", PV);
/* Elisione degli operatori, a scanf passiamo ancora
l'indirizzo di V */
Casi di errore frequenti
Errore 1 - Assegnazione fra puntatori incoerenti
char Yes = 'S';
int *P1;
char *P2 = &Yes;
...
P1 =P2;
/* Copia P2 in P1. Senza la conversione di P2 il
compilatore dà errore */
Errore 2 - Accesso a un'entità
tramite un puntatore incoerente
char Yes = 'S'; /* Occupa 1 byte
*/
char No = 'N'; /* Occupa 1 byte */
float F = 1.234; /* Occupa almeno 4 byte */
int *P1;
char * P2 = &Yes; /* Punta P2 a Yes */
P1 = (int *) P2;
/* Copia P2 in P1, quindi fa puntare a Yes pure P1,
tuttavia P1 serve
per indirizzare un'entità intera, non un'entità char. */
*P1 = 0;
/* Se un intero occupa 2 byte, questa istruzione
azzera sia Yes che No,
ma se un intero occupa 4 byte, si sconfina sui primi due byte di F. */
Errore 3 - Inesistenza
dell'entità che si intende puntare
int *P = &V;
/* Qui V non esiste ancora, verrà dichiarata subito
dopo */
int V = 1;
Errore 4 - Accesso tramite
un puntatore non inizializzato
int V = 1;
int *P1 = NULL; /* Puntatore annullato */
int *P2; /* Puntatore non correttamente riferito */
*P1 = 0; /* P1 non è
in grado di puntare da nessuna parte */
*P2 = 0;
/* P2 dove punta? Stiamo azzerando 2 o 4 celle di
memoria (secondo */
Puntamento a variabili strutturate
Anche qui ricorre il concetto di coerenza:
● per puntare a un record, occorre un puntatore
a struttura del medesimo tipo;
● per puntare a un vettore di interi occorre
un puntatore a intero;
● per puntare a un vettore di float occorre
un puntatore a float;
● per puntare a un vettore di caratteri
o a stringa occorre un puntatore a char;
● per puntare a un vettore di record, occorre
un puntatore a struttura.
Accesso indiretto a vettori
ATTENZIONE! : l'accesso indiretto a
un vettore contiene elementi nuovi, da analizzare attentamente:
int Vett[100];
int *P;
/* Puntatore a intero o a vettore di interi di qualsiasi
lunghezza */
Innanzitutto consideriamo il puntatore. P può indirizzare un
solo valore intero, mentre Vett ne contiene fino
a 100.
Dobbiamo forse pensare che P sia un puntatore incoerente rispetto a Vett?
No, in effetti "puntare a un vettore" equivale a "puntare al primo elemento
del vettore", ossia Vett[0], quindi appunto un singolo
elemento.
Di conseguenza P dev'essere, in generale, puntatore
a "entità basilare" del vettore: per una stringa o un vettore di char serve
un puntatore a char, per un vettore di float serve un puntatore a float,
per un vettore di record serve un puntatore alla medesima struttura base
che costituisce ogni elemento del vettore.
Per quanto detto finora, è certo che il puntamento a Vett
possa essere svolto almeno in questo modo:
P = &Vett[0]; /* Puntamento a Vett
*/
Per puntare a Vett, il linguaggio C ammette un'istruzione
equivalente, più "snella", ma anche meno comprensibile, e apparentemente
in contrasto con la regola del puntamento, poiché non si utilizza l'operatore
&:
P = Vett; /* Puntamento a Vett */
In teoria con il linguaggio C potremmo costruire il tipo "vettore di 100
elementi interi", e quindi un puntatore coerente con questo specifico array,
però è una complicazione inutile. Va osservato che se P
punta all'elemento
0-esimo di Vett, i successivi elementi si troveranno
immediatamente dopo lo 0-esimo, quindi è sufficiente puntare in testa al
vettore per individuare la posizione in memoria di tutto il vettore.
La domanda ora è: dato il puntamento allo 0-esimo elemento, come facciamo
a raggiungere i successivi?
È semplice: al puntatore P si accosta l'indice dell'elemento
che vogliamo raggiungere.
P[13] /* Indica
il 13-esimo elemento di Vett */
P[k] /* Indica il k-esimo elemento di
Vett (0 <= k < 100) */
Il vantaggio di questo tipo di gestione consiste nel poter utilizzare il
medesimo puntatore P per puntare a vettori di interi
di qualsiasi lunghezza. Ciò è molto utile, come vedremo, nel passaggio di
parametri a procedura/funzione. Ad esempio:
int Vett_1[100];
int Vett_2[50];
int * P; /* Puntatore a intero o a vettore di interi
di qualsiasi lunghezza */
int k;
…
P = Vett_1; /* Punta a Vett_1 */
/* Azzeramento dell'intero vettore Vett_1, acceduto
indirettamente */
for ( k = 0; k < 100; k++ ) P[k] = 0;
P = Vett_2; /* Punta a Vett_2 */
for ( k = 0; k < 50; k++ ) P[k] = 0; /* Azzeramento
dell'intero vettore Vett_2, acceduto indirettamente */
È assai frequente l'accesso indiretto a stringa. La stringa richiede un
puntatore a char. Ad esempio:
char S [ ] = "Prova"; /* Stringa di
5 caratteri significativi + 1 NUL */
char *P1 = &S[0]; /* Punta alla stringa S */
char *P2 = S; /* Anche P2 punta alla stringa S */
A proposito di stringhe, occorre accennare al fatto che una stringa costante
(ad esempio "Prova"), secondo una regola del linguaggio C viene allocata
in memoria statica da parte
del compilatore, quindi tutte le costanti stringa verranno a trovarsi nel
segmento dei dati globali. Ad esempio:
if(strcmp(S,"ciao")== 0) printf("saluto");
"ciao" e "saluto" verranno allocate nel segmento dati globali come variabili
stringa senza nome, e saranno accessibili soltanto da parte del codice delle
funzioni "strcmp" e "printf ".
Il linguaggio C permette di allocare costanti stringa o in appositi vettori
oppure sottoforma di stringhe senza nome. Per poter accedere liberamente
a una stringa senza nome, è necessario dichiararla contestualmente al relativo
puntatore:
char *Punt = "ciao"; /* La stringa
"ciao" sarà accessibile solo tramite Punt */
Accesso indiretto ai record
struct Point { /* Dichiara una struttura
con due campi */
int X, Y;
} Origine; /* Dichiara una variabile record, strutturata
"Point" */
struct Point *Punt; /* Dichiara un puntatore a "entità
record strutturata Point" */
Punt = &Origine; /* Punta alla variabile record "Origine"
*/
Punt -> X = 0;
/* Azzera il campo X del record Origine, acceduto
indirettamente
(ricordiamo che l'accesso diretto sarebbe "Origine.X = 0;") */
Punt -> Y = 0;
/* Azzera il campo Y del record Origine, acceduto
indirettamente
(ricordiamo che l'accesso diretto sarebbe "Origine.Y = 0;") */
Attenzione: si può evitare di utilizzare
l'operatore "freccia". Osservando che il puntamento a un record è identico
al puntamento a una variabile semplice, viene da pensare che sia identica
pure la sintassi per eseguire l'accesso indiretto:
*Punt.X = 0;
L'idea non è completamente sbagliata, però è realizzata male, causa le priorità
degli operatori:
il delimitatore "." ha priorità
sull'operatore di indirizzamento
" * ", di conseguenza l'istruzione equivale a:
*(Punt.X) = 0;
Dato che Punt non è una variabile record bensì un
puntatore, il campo X non appartiene a Punt.
Pertanto l'espressione Punt.X è errata.
Utilizzando le parentesi, tuttavia, si può ottenere la priorità corretta:
(*Punt).X = 0;
Liste
La principale applicazione dei puntatori associati alle strutture di dati
consiste nella creazione di liste.
Possiamo assimilare una lista ad una struttura (vettore di record) di lunghezza
indefinita.
Il codice minimo per realizzare una lista a puntatori è il seguente:
#include<iostream>
using namespace std;
struct T{
int x;
struct T *y;
};
main(){
T *p,*q=NULL,*j=NULL;
bool primo=true;
//caricamento
do{
p=new T;
cout<<"ins:";cin>>p->x;
p->y=NULL;
if(primo){
j=p;
q=p;
primo=false;
}else{
q->y=p;
q=p;
}//fine if
}while(p->x);
//scrittura a video
p=j;//p punta al primo elemento con j
while(p->x){
cout<
p=p->y;
} //fine while
} // fine main
Viene dichiarato una struttura record costituita da una variabile intera x ed una variabile puntatore ad un'altra struttura T che chiamiamo y
Il puntatore x va a puntare la parte intera di un record: mentre il puntatore p->y punta a NULL ( ed è comunque inizializzato).
Solo se siamo al primo giro del ciclo do, i tre puntatori vengono riferiti allo stesso elemento.
Nel secondo giro viene allocato lo spazio per un nuovo elemento: p=new T; p punta alla parte intera del nuovo elemento entrante mentre con l'istruzione: q->y=p; consentiamo al primo elemento di riferirsi al secondo.
Sempre al secondo giro (e anche in quelli successivi) è necessario un ulteriore movimento: q=p; in questo modo a j rimane da solo a puntare al primo elemento.
Al terzo giro p punta al nuovo elemento entrante mentre il secondo elemento di struttura T punterà al terzo tramite l'istruzione: q->y=p.
poi q viene riposizionato sull'ultimo elemento assieme a p con l'istruzione: q=p.
Nei cicli successivi questo comportamento si ripete, va avanti finché
l'utente non inserisce come valore 0.
La scansione della lista viene effettuata portando p a puntare al primo
elemento con l'istruzione:
p=j;
la stampa è eseguita dal ciclo while
while(p->x){
cout<
p=p->y;
}
incrementando ogni volta la posizione di p nella lista con:
p=p->y;
Stack/Heap
Lo stack è un'area di memoria fissa, la sua dimensione viene usata al momento
della compilazione.
Lo heap è un'area ausiliaria che può essere impostata al moento dell'esecuzione
(memoria virtuale).
La memoria viene allocata quando il programma lo richiede, questo si ottiene
con l'uso di variabili puntatore.
L'heap viene usato per dati temporanei o per dati la cui dimensione è sconosciuta
prima dell'esecuzione.
Un esempio di uso dello stack:
#include<iostream.h>
using namespace std;
const int max=20;
main(){
char s[max];
strcpy(s,"pippo");
cout <<s;
}
Un esempio di uso dello heap:
#include<iostream.h>
main(){
char *s;//imposta una var. puntatore
s=new char;/*recupera memoria dallo heap,
new restituisce un puntatore ad una allocazione
di memoria nello heap e inserisce questo indirizzo nella var.
puntatore*/
strcpy(s,"pippo");/*si copiano alcuni dati in quest'area
di memoria occupando solo i byte necessari, in tal caso 5
byte e un carattere terminatore, in totale 6 byte
n.b. la forma si puo compattare in:
char *s=new char;*/
cout<<s;
}
Quando la memoria è allocata, essa rimane tale finchè non si dispone diversamente.
A questo punto si può usare delete. Se non si restituisce
la memoria dinamica allo heap quando si è terminato di usarla si può superare
la capacità di memoria disponibile e si rileva che il sistema comincia a
rallentare e a bloccarsi.
#include<iostream.h>
main(){
char *s;
s=new char;
strcpy(s,"pippo");
cout<<s;
delete(s);
}