Dopo uno stop di 2 settimane, riprendiamo con un argomento alquanto corposo: i puntatori, gioia e dolore di ogni sviluppatore, che sia alle prime armi oppure esperto.
In C++, la memoria di un computer viene vista come una successione di celle di memoria, ognuna grande 1 byte (esattamente quanto un char) ed ognuna con un indirizzo unico. Queste celle di memoria di 1 byte sono ordinate in modo tale da permettere l'utilizzo di celle consecutive di memoria per rappresentare dati con una dimensione maggiore di 1 byte (esempio: int - 4 byte, float - 4 byte, double - 8 byte).
In questo modo, ogni cella viene identificata attraverso il suo indirizzo unico. Ad esempio, la cella di memoria con indirizzo 2000 viene sempre preceduta da una cella con indirizzo 1999 e seguita da una con indirizzo 2001.
Generalmente, un programma C++ non specifica attivamente gli indirizzi esatti di memoria delle variabili da allocare: fortunatamente, questo compito viene lasciato all'ambiente in cui il programma e´ eseguito, ovvero il sistema operativo. Tuttavia, a volte conoscere l'indirizzo di una variabile all'interno di un programma C++ porta ad alcuni particolari vantaggi.
L'operatore di referenziazione: &
L'indirizzo di una variabile salvata in memoria puo´ esser ottenuto mediante l'operatore &, detto di referenziazione (suona malissimo in italiano, ma in inglese si dice reference o address-of operator).Ad esempio, il seguente codice:
int a = 10; int x = &a;
permette di salvare nella variabile x l'indirizzo della variabile a. In altre parole, se l'indirizzo di memoria della variabile a e` 2000, allora la variabile x assume valore 2000 e non 10, che rappresenta invece il valore salvato all'indirizzo 2000.
Se avessi voluto copiare il valore contenuto in a, ovvero il valore 10, avrei dovuto scrivere semplicemente x = a.
La variabile che contiene l'indirizzo di un'altra variabile (come x nel precedente esempio) viene chiamata puntatore in C++. Un puntatore permette di eseguire operazioni di basso livello oppure utilizzare oggetti in altri punti di un programma, senza necessariamente copiarli.
L'operatore di dereferenziazione: *
Un puntatore puo´ esser usato per accedere alla variabile a cui esso punta.Tale operazione e´ possibile tramite l'operazione di dereferenziazione, ovvero facendo precedere * all nome della variabile. Tale operatore puo´ esser letto come "valore puntato da".
Considerando sempre l'esempio precedente, se scrivo:
int y = *x;
accedo al valore puntato da x, ovvero al valore salvato all'indirizzo di memoria contenuto in x, in questo caso 10. Se avessi scritto y = x, avrei salvato in y il valore 2000.
Uso dei puntatori
Un puntatore viene dichiarato in C++ nel seguente modo:char *a; //non inizializzato int *b; //non inizializzato float *c = nullptr; //inizializzato con nullptr double *d; //non inizializzato double *e, f; //non inizializzato
Scrivendo in questo modo, i puntatori non sono inizializzati, ovvero non contengono alcun valore preciso. Una dichiarazione piu´ corretta prevede di assegnare il valore nullptr (in C invece e` NULL, ossia 0) al puntatore.
L'ultima riga indica la dichiarazione di un puntatore, *e, ed una variabile double, f. L'operatore * si riferisce sempre al nome della variabile. Alcuni preferiscono scrivere
double * e;
ma spesso, puo´ portare a confusione, come nel caso dell'esempio precedente e quindi e´ preferibile non lasciar spazi.
I puntatori sono molto utili nella manipolazione di array. In particolare, le seguenti operazioni sono valide:
int array[10]; int *puntatore; puntatore = array;
Infatti, l'ultima operazione permette di assegnare al nostro puntatore l'indirizzo del primo elemento dell'array. Tale assegnazione equivale a scrivere:
puntatore = &array[0];
e grazie a questa caratteristica, e` possibile spostarsi all'interno di un array per mezzo del puntatore in modo libero, ma bisogna fare attenzione:
puntatore = array; // equivale a puntare ad array[0] puntatore++; //equivale a puntare ad array[1], puntatore = puntatore - 2; //ops!
Infatti, l'ultima operazione, nonstante sia legittima, assegna al puntatore un indirizzo di memoria, che precede il vettore stesso e che potrebbe non esser valido (ossia, non contenente dati). Nel caso in cui volessimo accedere al valore contenuto a tale indirizzo di memoria, otterremmo un misero crash del nostro programma.
Anche una stringa di caratteri e` un array, quindi scrivere
char *puntatore = "Ciao a tutti gli amici di Lubit!";
equivale a salvare in puntatore l'indirizzo di memoria in cui e´ stata memorizzata il carattere "C". Quindi, se ad esempio la stringa di caratteri inizia dall'indirizzo 1000, puntatore assume il valore 1000, indirizzo di memoria in cui e´ salvato il carattere "C".
Ancora, e´ possibile avere un doppio puntatore, ovvero un puntatore che punta ad un altro puntatore. Per creare un doppio puntatore, si aggiunge un * aggiuntivo:
int a = 10; int b = &a; //puntatore ad a int **c = nullptr; //doppio puntatore, non inizializzato c = &b; //c punta all'indirizzo di b, che contiene il riferimento ad aIn questo modo si aggiunge un ulteriore livello di indirezione.
Con i puntatori, come vedremo in seguito, si possono passare ad una funzione i parametri per indirizzo anziche´ per valore, ovvero senza la necessita´ di copiarli. Inoltre, i puntatori sono fondamentali per tener traccia di zone di memoria allocate dinamicamente, argomento questo, che vedremo nella prossima puntata!
A presto!