Abbiamo detto che ogni variabile è un riferimento ad un'area di memoria e che, quando accediamo alla variabile, in realtà stiamo accedendo a quell'area di memoria.
Perciò memorizzare un dato in una variabile significa memorizzare quel dato nell'area di memoria a cui la variabile fa riferimento.
Abbiamo anche detto che i puntatori invece non memorizzano dati, ma indirizzi di memoria.
Ma dove vengono memorizzati questi indirizzi di memoria? Semplice, in un'altra area di memoria.
Prima di proseguire bisogna dire che la gestione degli indirizzi di memoria varia da architettura ad architettura.
Ad esempio in un Intel a 64 bit, un indirizzo di memoria è composto da 64 bit (da qui il nome dell'architettura), il che implica la possibilità di utilizzare una quantità maggiore di memoria RAM delle architetture a 32 bit.
Infatti con 32 bit è possibile fare riferimento ad un massimo di 3 GB di memoria. In un sistema a 32 bit sarà impossibile fare riferimento alle locazioni successive al 3 GB di memoria.
In un sistema a 64 bit quindi, un indirizzo di memoria è formato da 64 bit.
Per scriverlo in binario ci vorrebbero 64 cifre, perciò è conveniente utilizzare la numerazione esadecimale.
Un esempio di indirizzo di memoria a 64 bit può essere "7FFFAA764CA8".
Ora, un puntatore è una variabile particolare, nella quale vengono memorizzati indirizzi di memoria.
Quando creiamo una comune variabile, ad esempio di tipo char, sappiamo che viene allocato uno spazio di memoria di 8 bit, perché è la quantità di memoria massima necessaria per poter memorizzare dei caratteri. Allo stesso modo una variabile di tipo long allocherà uno spazio di 64 bit, per poter memorizzare numeri molto lunghi.
Un puntatore invece, deve memorizzare indirizzi di memoria. Perciò quanto sarà grande la memoria allocata per contenere tali indirizzi? Semplice, 64 bit (o 32 in un'architettura a 32 bit).
Prima di fare una prova, chiariamo che un puntatore, essendo a modo suo una variabile che memorizza indirizzi, ha bisogno di uno spazio in memoria dove poter memorizzare questi indirizzi.
Ad esempio:
char varChar;
char* puntChar;
puntChar = &varChar;
Ora il puntatore puntInt contiene l'indirizzo di memoria di varInt, ma dove salva questo indirizzo?
Chiaramente in un'altra area di memoria, allocata durante la dichiarazione del puntatore.
Riscriviamo lo stesso programma, ma con dei commenti chiarificatori:
char varChar; //alloca un'area di memoria di 8 bit (è di tipo char)
char* puntChar; //alloca un'area di memoria di 64 bit per poter contenere un indirizzo di memoria
puntChar = &varChar; //ora puntChar contiene l'indirizzo di varChar
L'ultima riga si può leggere anche così:
"l'indirizzo di varChar viene memorizzato nell'area di memoria a cui fa riferimento puntChar"
Essendo puntChar, una variabile puntatore, avendo quindi anch'essa un'area di memoria dove risiedono i dati (gli indirizzi) memorizzati attraverso puntChar, è chiaro che è del tutto legittimo utilizzare l'operatore di indirizzo & per conoscere l'indirizzo di memoria in cui inizia quest'area di memoria:
cout << puntChar; //stampa l'indirizzo a cui punta puntChar (e cioé quello di varChar)
cout << &puntChar; //stampa l'indirizzo di puntChar
Nel mio caso i due cout stampano rispettivamente:
/> 7fff4e806a2c
/> 7fff4e806a10
Il primo è l'indirizzo della variabile varChar, a cui punta puntChar.
Il secondo è l'indirizzo di puntChar, in cui è memorizzato l'indirizzo di varChar.
Se andassimo a vedere nei 64 bit di memoria a partire dall'indirizzo 7fff4e806a10, vedremmo che quest'area di memoria contiene l'indirizzo 7fff4e806a2c.
In altre parole la seguente istruzione:
cout << puntChar;
significa:
"vai nei 64 bit di memoria a partire dalla locazione 7fff4e806a10 (l'indirizzo di puntChar) e stampa il suo contenuto, cioé 7fff4e806a2c (l'indirizzo di varChar)"
La seguente istruzione invece:
cout << *puntChar;
significa:
"vai nei 64 bit di memoria a partire dalla locazione 7fff4e806a10, recupera il contenuto di quest'area di memoria, cioé 7fff4e806a2c, e stampa il contenuto dell'area di memoria di 8 bit (perché puntChar punta a char) a partire dalla locazione di memoria indicata da quest'ultimo indirizzo"
Il programma va quindi nell'area di memoria di puntChar, recupera l'indirizzo in essa contenuto, va a questo indirizzo e recupera il contenuto della locazione di memoria indicata da quest'ultimo, che chiaramente è la stessa a cui fa riferimento varChar, dal momento che puntChar punta a quella variabile.
So che all'inizio può sembrare complicato, l'importante è sperimentare, fate tante prove con i puntatori, stampate il loro contenuto a video, vedete cosa contengono, stampate gli indirizzi dei puntatori stessi, insomma smanettate, e vedrete che con la pratica vi sarà tutto più chiaro.
Prima di passare all'aritmetica dei puntatori, vi voglio dare una prova che al momento della dichiarazione di un puntatore viene allocata un'area di memoria di 64 bit (32 bit per le architetture omonime).
Ricordate il fatto che gli elementi di un array sono contigui in memoria, e cioé l'uno successivo all'altro?
Basterà creare un array di puntatori e guardare quante locazioni di memoria ci sono tra un elemento ed il successivo. Poi sapendo che ogni locazione è di 8 bit, basterà una semplice moltiplicazione per capire quanta memoria viene allocata:
char* arrayPuntatore[10];
cout << &arrayPuntatore[0]; << endl;
cout << &arrayPuntatore[1]; << endl;
Nel mio caso viene stampato a schermo:
/> 0x7fff4e806a10
/> 0x7fff4e806a18
Contiamo quante locazioni ci sono tra il primo indirizzo ed il secondo:
7fff4e806a18 - 7fff4e806a10 = 8
Il secondo indirizzo è l'indirizzo di una locazione di memoria che si trova 8 locazioni più avanti di quella indicata dal primo indirizzo.
8 locazioni significa (considerando che ogni locazione è di 8 bit) 8 * 8 = 64 bit.
Questa è la conferma che un puntatore richiede l'allocazione di un'area di memoria di 64 bit, quanto basta per poter memorizzare un indirizzo di memoria.
Per avere l'ultima conferma, il C++ ci ha dato un operatore, chiamato sizeof, che restituisce la grandezza in byte (1 byte = 8 bit) di una variabile:
int grandezza;
grandezza = sizeof(arrayPuntatore[0]); //la funzione sizeof restituisce la grandezza di arrayPuntatore[0]
cout << grandezza; //restituisce 8 byte, cioé 64 bit
Avremmo anche potuto scrivere così:
cout << sizeof(arrayPuntatore[0]);
Spero che sia tutto chiaro, ma se così non fosse non vi scoraggiate, con la pratica diventerà semplice come bere un bicchiere di bourbon.
Per ora chiudiamo qui e rimandiamo la trattazione dell'aritmetica dei puntatori al prossimo post. Se non vi è chiara l'utilità dei puntatori, sappiate che lo sarà quando parleremo delle funzioni ma soprattutto quando arriveremo a parlare di classi e oggetti nella programmazione object oriented.
E' anche vero che non è necessario conoscere il meccanismo che sta dietro un puntatore per usarlo, ma è altrettanto vero che in questo modo il loro uso sarà limitato da una comprensione parziale dell'argomento, mentre se vogliamo avere il massimo dai puntatori è meglio comprenderne il meccanismo d'azione, anche per evitare di incappare in bugs ed errori nei nostri programmi.
Ripeto, se non li avete capiti bene non vi demoralizzate, non vi arrendete, all'inizio è così per tutti, ci vuole solo un po' di tempo e pratica per assimilarli bene.
Nessun commento:
Posta un commento