edutecnica

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<x<<" ";
   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<x<<" ";
   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);
}