Introduzione ai puntatori Algoritmicamente November 19, 2009 http://algoritmicamente.wordpress.com/ 1 Concetti fondamentali Nei linguaggi quali C, C++, il concetto e l'uso dei puntatori è estremamente importante, non a caso la maggior parte dei libri che trattano tali linguaggi, sottolineano come l'uso dei puntatori sia centrale nella cultura di un buon programmatore. Prima di denire i puntatori, c'è bisogno di analizzare la struttura e il modo con cui il sistema operativo gestisce la memoria di un generico programma quando questo è in esecuzione. Osserviamo queste due dichiarazioni: int a = 3; char b = 'c'; la prima dichiarazione, chiamata assegnazione, non fa altro che riservare una porzione di memoria predisposta a contenere il valore numerico '3', indicata con l'etichetta 'a': questa operazione è chiamata allocazione statica. Il termine statico è utilizzato in quanto il numero di variabili necessarie e lo spazio di cui necessitano sono decisi a priori, cioè dopo la compilazione non sarà possibile creare nuove aree di memoria da utilizzare; tali variabili verranno deallocate (distrutte) solo al termine dell'esecuzione del programma. Tutte le variabili allocate staticamente vengono memorizzate in fase di esecuzione in una particolare area di memoria detta stack. La sua struttura si presenta nel seguente modo: La sua struttura è quella di una pila, nella quale è contenuto il valore della variabile. Ogni cella è univocamente identicata da un indirizzo esadecimale. Nel caso di un array: 1
int c[3] = {0}; nello stack i valori di tale array vengono memorizzati in indirizzi contigui di memoria. Queste variabili statiche, una volta istanziate nello stack, rimangono nell'area di memoria a loro dedicata no al termine del programma, dopodichè quell'area di memoria verrà liberata. 2 I puntatori Un puntatore è un tipo speciale di variabile che, invece di contenere un valore, contiene l'indirizzo di una locazione di memoria. Per dichiarare una variabile di tipo puntatore, si utilizza l'operatore '*', che può stare indierentemente subito dopo il tipo di variabile, o subito prima il nome di quest'ultima: int* p; // int *p; In questo caso, p è un puntatore ad intero. Si noti che un puntatore di un certo tipo, può puntare solo a oggetti di quello stesso tipo. Consideriamo ora questo esempio: int n=5; int* p=&n; // alias In termini più formali tale assegnazione '&n' indica la creazione di un alias, un riferimento alla variabile 'n'. Dunque ogni volta che la variabile 'p' verrà utilizzata o modicata, tali modiche andranno a ripercuotersi anche sulla variabile 'n'. Aiutandoci con qualche immagine, vediamo cosa accade: In questo caso, abbiamo n, che è una variabile di tipo intero a cui è stato assegnato il valore 5, e abbiamo un puntatore ad intero, a cui è stato assegnato un indirizzo di memoria, e cioè quello di n. Ricordiamo che il simbolo '&' seguito dal nome di una variabile, restituisce l'indirizzo di quest'ultima. Se facciamo stampare su schermo quello che realmente abbiamo fatto: 2
cout << Contenuto di n: << n << endl; cout << Indirizzo di n: << &n << endl; cout << Contenuto di p: << p << endl; cout << Contenuto dell'oggetto puntato da p: << *p << endl; otterremo il seguente output: Contenuto di n: 5 Indirizzo di n: 0x898d008 Contenuto di p: 0x898d008 Contenuto dell'oggetto puntato da p: 5 Si noti che, come abbiamo detto in precedenza, p contiene l'indirizzo di memoria di n, ovvero '0x898d008'. Il valore assegnato ad n è 5, e per ottenere questo valore tramite il puntatore, utilizziamo l'operatore '*', che assegnato in questo caso a p restituisce 5. 3 Heap e allocazione dinamica della memoria Nel paragrafo 1 abbiamo introdotto la gestione della memoria all'interno dello stack da parte del sistema operativo, considerando delle variabili che abbiamo denito statiche. Esiste tuttavia un'altra area di memoria, distinta dallo stack, che si chiama heap: è qui che vengono istanziate delle variabili che prendono il nome di dinamiche. Il termine dinamiche non è un termine scelto a caso, infatti in questa parte di memoria le variabili allocate hanno una vita indenita, possono essere allocate o deallocate durante l'esecuzione del programma, a differenza di quanto accade invece per lo stack. Possiamo ragurare la sua struttura come quella dello stack: Esiste comunque una proprietà che lega heap e stack: Allocando memoria in una delle due aree, questa aumenta mentre l'altra diminuisce, rientrando ovviamente nei limiti imposti dalla quantità di memoria disponibile. 3
Vediamo dunque come creare delle variabili dinamiche utilizzando i puntatori. La sintassi è la seguente: int* p=new int ( ); L'operatore 'new', ci permette di istanziare delle variabili nell'heap. Ricordiamo che se il puntatore è di un certo tipo, la variabile che si istanzia nell'heap deve essere necessariamente di quel tipo; nel nostro caso, la nostra variabile è di tipo intero. Nell'esempio abbiamo semplicemente riservato una parte di memoria che andrà successivamente ad occupare un intero; abbiamo quindi creato un puntatore ad intero che punta ad un indirizzo di memoria nell'heap dove per ora verrà messo il valore 0. Vediamo gracamente cosa accade: Per assegnare un valore a 'p' o modicarne uno già esistente, basterà fare: *p=5; Adesso l'indirizzo di memoria a cui punta 'p', conterrà il valore 5. Abbiamo detto in precedenza che il fattore che distingue una variabile statica da una dinamica è che quest'ultima può essere allocata (creata) o deallocata (distrutta) durante l'esecuzione del programma. Se vogliamo per esempio deallocare l'area di memoria a cui 'p' punta, utilizziamo l'operatore delete: delete p; Scriviamo ora un esempio di programma che ci faccia capire meglio cosa accade: int main() { int* p = new int (); //Allochiamo p cout << *p << ; //Stampiamo il contenuto di p *p = 5; //Assegnamo il valore 5 a p cout << *p << ; //Stampiamo il contenuto di p delete p; //Deallochiamo p cout<<*p<<endl; //Stampiamo il contenuto di p 4
} return 0; L'output del programma sarà: 0 5 0 Nel prossimo articolo spiegheremo come creare array e matrici dinamiche. 5