venerdì 24 aprile 2009

Note di C++ : i puntatori

 

I puntatori rappresentano uno dei punti di forza più rappresentativi del C++. Al tempo stesso, però, spesso sono causa di confusione per i neofiti a causa del loro meccanismo leggermente complesso.

Per poter capire a cosa serve un puntatore, è necessario sapere che la memoria del calcolatore in cui possono risiedere le variabili è suddivisa in due parti:

  • lo Stack
  • e l'Heap

Nello Stack vengono immagazzinate tutte le variabili, come quelle dei programmi che abbiamo visto finora. Ad esempio quando si definisce:

int i = 0;

Il computer riserverà due byte di memoria dello Stack per la variabile i. Dichiarazioni di questo tipo sono abbastanza "statiche", in quanto non permettono ad esempio di modificare il numero di byte assegnato ad una variabile o di deallocare la memoria di una variabile, quindi di liberare lo spazio precedentemente assegnato.

Quest'ultima osservazione può rappresentare un problema quando si manipolano grosse quantità di informazioni, in quanto potrebbe succedere che lo Stack si riempia e quando si tenta di allocare altra memoria si verifichi il cosiddetto Stack overflow.

Per permettere al programmatore di utilizzare la memoria in maniera "dinamica" è possibile allora utilizzare la memoria Heap, detta anche memoria dinamica. Il termine dinamica sta proprio ad indicare che è data la possibilità al programmatore di allocare e deallocare la memoria a suo piacimento.

Questa, tuttavia, è un'operazione molto delicata che, se compiuta in modo errato può portare ad errori spesso difficili da trovare che si verificano in fase di esecuzione. La manipolazione dell'Heap avviene proprio tramite i puntatori.

Una variabile puntatore è una variabile che contiene l'indirizzo di memoria di un'altra variabile. Ad esempio, si supponga di avere una variabile intera chiamata x ed un'altra variabile (quella puntatore appunto) chiamata p_x che è in grado di contenere l'indirizzo di una variabile di tipo int. In C++, per conoscere l'indirizzo di una variabile, è sufficiente far precedere al nome della variabile l'operatore &. La sintassi per assegnare l'indirizzo di una variabile ad un'altra variabile è la seguente:

p_x = & x;

In genere, quindi, una variabile che contiene un indirizzo, come ad esempio p_x, viene chiamata variabile puntatore o, più semplicemente, puntatore. Per chiarire ulteriormente questo concetto, guardiamo la figura seguente:

clip_image001

Supponiamo che la variabile intera x, raffigurata nel riquadro di sinistra, sia allocata all'indirizzo di memoria 7500. La variabile p_x, ovvero la variabile puntatore, inizialmente non contiene alcun valore (ovvero nessun indirizzo).
Nel momento in cui viene eseguita l'istruzione:

p_x = & x;

la variabile puntatore conterrà l'indirizzo della variabile x. Cioè:

clip_image002

La freccia viene tracciata dalla cella che conserva l'indirizzo verso la cella di cui si conserva l'indirizzo.
Per accedere al contenuto della cella il cui indirizzo è memorizzato in p_x è sufficiente far precedere alla variabile puntatore il carattere asterisco (*). In tal modosi è deindirizzato il puntatore p_x.

Ad esempio, se si prova ad eseguire le due istruzioni seguenti:

p_x = & x;
*p_x = 20;

il valore della cella x sarà 20. Graficamente otterremo:

clip_image003

Si noti che se p_x contiene l'indirizzo di x, entrambe le istruzioni che seguono avranno lo stesso effetto, ovvero entrambe memorizzeranno nella variabile x il valore 20:

x = 20;
*p_x = 20;

Naturalmente, anche le variabili puntatore come tutte le altre richiedono una definizione per ogni variabile. L'istruzione che segue definisce una variabile puntatore (p_x) che è in grado di contenere l'indirizzo di una variabile int:

int* p_x

Si potrebbe vedere tale dichiarazione come formata da due parti. Il tipo di x:

int*

e l'identificatore della variabile:

p_x

L'asterisco che segue il tipo int può essere interpretato come: "puntatore a" ovvero il tipo:

int*

dichiara una variabile di tipo "puntatore a int".

Naturalmente, è possibile definire variabili puntatori per ogni tipo. Così, potremo scrivere:

char* p_c;
float* p_f;
double* p_d;

E' molto importante tenere presente che se un programma definisce un puntatore di un determinato tipo e poi lo utilizza per puntare ad un oggetto di un altro tipo, si potranno ottenere errori di esecuzione e avvertimenti in fase di compilazione. Anche se il C++ permette simili operazioni è comunque buona norma di programmazione evitarle.

Anche per le variabili puntatore esiste la possibilità di ricorrere all'inizializzazione nel momento della loro definizione. Ad esempio:

int x;
int* p_x = & x;
char c ;
char* p_c = & c;

Il C++ mette a disposizione un tipo particolare di inizializzazione di un puntatore. Infatti è possibile, ed anzi è buona norma farlo, inizializzare una variabile puntatore a NULL (ovvero non la si fa puntare a nessun indirizzo di memoria) .
Ad esempio:

int* p_x = NULL;
char* p_c = NULL;

Quando però si inizializza un puntatore a NULL, si deve tener presente che non è possible in alcun modo utilizzare la variabile puntatore per nessuna operazione finchè non la si inizializzi con un indirizzo valido.
Infatti, se provassimo a scrivere:

int* p_x = NULL;
cout << *p_x << endl;
// Errore!!

otterremmo un errore in esecuzione in quanto la variabile puntatore p_x non punta nessuna variabile intera che contenga un valore valido.
Occorrerà far puntare p_x ad una variabile intera:

int x = 20;
int* p_x = NULL;
......
......
......
p_x = & x;
cout << *p_x << endl;
// Adesso è corretto

Esiste ancora un'altra possibilità per inizializzare una variabile puntatore: utilizzando l'operatore new messo a disposizione dal C++.
Quando si scrive:

int x = 20;
int* p_x = NULL;
......
......
p_x = new int;
*p_x = x;

quello che si ottiene è un risultato apparentemente simile a quello visto negli esempi precedenti, ma in realtà abbastanza diverso.
L'istruzione:

p_x = new int

altro non fa che allocare una quantità di memoria necessaria per contenere un int (ovvero 2 byte) ed assegnare l'indirizzo del primo byte di memoria alla variabile p_x. Questo indirizzo è certamente diverso da quello in cui è contenuta la variabile x stessa, per cui in tal caso avremo in realtà due variabili intere differenti che contengono lo stesso valore.
E' anche possibile usare l'istruzione:

p_x = new int(20);

per inizializzare direttamente il valore *p_x a 20.

Si faccia ora attenzione. Abbiamo detto che le variabili puntatore fanno riferimento alla memoria dinamica (heap). Diversamente dalla memoria statica, tutte le variabili puntatore allocate dal programmatore devono essere poi distrutte quando non servono più, per evitare i cosìdetti "memory leaks", ovvero per evitare che la memoria allocata resti tale anche dopo la terminazione del programma.
L'istruzione del C++ che serve per distruggere una variabile puntatore è: delete.
Nel caso dell'esempio precedente, avremmo:

int x = 20;
int* p_x = NULL;
......
......
p_x = new int;
*p_x = x;
......
......
delete p_x;
// La variabile p_x non serve più. Possiamo deallocarla
......
......

Per capire cosa è un memory leak, vediamo l'esempio seguente:

int x = 20;
int* p_x = NULL;
......
......
p_x = new int;
p_x = & x; // Attenzione!! Memory leak!!
*p_x = 20;
......

Perchè quando si esegue l'istruzione p_x = new int si sta creando un memory leak? La risposta è semplice. La variabile p_x è stata innanzitutto inizializzata utilizzando l'istruzione p_x = new int.
Tale istruzione ha fatto sì che venissero allocati due byte ed assegnato l'indirizzo del primo di questi byte alla variabile p_x. Quando poi si esegue l'istruzione p_x = & x la variabile p_x viene fatta puntare all'indirizzo in cui è contenuta la variabile x ma in nessun modo viene deallocata la memoria che era stata allocata in precedenza. Questo causa il famoso memory leak.
Bisogna stare molto attenti ai memory leak. Spesso non sono semplici da riconoscere e possono causare problemi ai programmi causando una graduale diminuzione delle risorse di sistema.
Per evitare il memory leak di prima avremmo dovuto scrivere:

int x = 20;
int* p_x = NULL;
......
......
p_x = new int;
delete p_x; // Deallocazione della memoria allocata da p_x
p_x = & x; // Corretto!
*p_x = 20;
...

Nessun commento:

Posta un commento