sabato 31 gennaio 2015

Modern C++ - I Puntatori

Bentornati sulle pagine di Lubit - The Secrets of Ubuntu.
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 a
In 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!


Nessun commento:

Posta un commento