|
ATTIVITÀ
:
Programmare
:
Linguaggi
:
Linguaggio C
:
Puntatori
Puntatori
I puntatori del C (e del C++) sono comodi, utili e facili da imparare, ed una volta imparati, oltre tutto, non si scordano più come l'equilibrio della bicicletta o dei pattini, e si usano con disinvoltura. Non
potete ignorarli, sono troppo divertenti...
Indice
- Perché tanto odio?
- Cosa sono i puntatori
- Fico, non sembrano tanto antipatici
- Bibliografia & Links
- Copyright e Licenza
Perché tanto odio?
Se qualcuno di voi ha mai letto Totem Comics, ricorderà per forza un disegnatore di nome Edika. La sua sezione nella rivista era appunto intitolata "Perché tanto odio?". Goliardia a parte, non sembra
inopportuno parlare dei puntatori cominciando da questa domanda. I puntatori sono antipatici a molti, un po' come le sbucciature sulle ginocchia quando impariamo ad andare in bicicletta senza le rotelle, ma
quando abbiamo imparato, tutti troviamo assolutamente divertente andare in bicicletta, e troveremmo le rotelle solo un impiccio. L'antipatia delle sbucciature è solo un lontano ricordo, spesso rimosso,
quando viene bilanciata dal divertimento delle curve a coltello o delle discese a manetta. Preso il via, sta poi a noi decidere se usare la bicicletta solo per rilassanti passeggiate in pianura o per massacranti
e gratificanti gare di ciclocross. Quindi la tesi è: i puntatori sono solo antipatici all'inizio, ma poi è una goduria? Si, io la vedo così. Ah, beh, allora anche tu ammetti che i
puntatori siano antipatici. OK, i puntatori sono antipatici, ma solo all'inizio e non per colpa loro. Chiediamoci perché. La colpa è di coloro che hanno scritto il C (ed il C++), ovvero per il
modo in cui hanno definito la sintassi del linguaggio che opera sui puntatori. Nella lingua italiana, o meglio nel linguaggio Italiano, gli stranieri trovano difficoltoso soprattutto l'uso di termini che assumono
un senso diverso rispetto al contesto in cui sono utilizzati. Facciamo subito un esempio: ancora e ancora. Anzi, dovremmo scrivere àncora ed
ancóra, e comunque anche noi italiani avremmo primo, difficoltà a leggere e secondo a collocare queste due parole. La prima, è il sostantivo che indica il pezzo di ferro con cui le
navi attraccano in mare aperto, il secondo è l'avverbio che indica continuazione temporale, facciamo un esempio: "La nave Alvaro è all'ancora a poche miglia dalla costa", e "Acquisterei ancora un
Mac anche se ne ho già due". Aaahh!? ecco?! Antipatico no? Beh, chi ha scritto il C (ed il C++) ha operato la stessa cattiveria nei confronti dei puntatori, per cui le tre funzioni che essi
forniscono, le forniscono con due soli simboli. Voi direte: avevano finito i simboli? No, è proprio cattiveria...

Cosa sono i puntatori
Cosa volete che siano? Sono delle cose divertenti rese antipatiche dalla cattiveria di chi li ha invevntati. Si, vabbè, non come sono, cosa sono. V'accontento subito: sono delle variabili che
contengono un indirizzo di memoria. A cosa serve una variabile che contiene un indirizzo? Beh, serve a fare tante cose, la prima delle quali è risparmiare memoria. Ah, bella, io dichiaro una
variabile che contiene un indirizzo di memoria, che magari contiene dei dati. Se usavo una variabile che identificava quei dati non facevo prima e sprecavo meno memoria? No. Facciamo un esempio, ma solo per
gradire. Mettiamo che voi abbiate definito una variabile che contiene un numero. Mettiamo anche che abbiate definito un'altra variabile, un puntatore, che contiene l'indirizzo della prima, che ricordiamo essere un
numero; parleremo in questo caso di puntatore ad un numero, ed in particolare se il numero è un intero, parliamo di puntatore ad intero. Se il numero è a virgola mobile, parleremo di puntatore a
numero in virgola mobile, ecc. OK, poi? Poi cerchiamo di usare la prima variabile (il numero). Cosa avviene? Avviene che se vogliamo sommare a quel numero un altro numero, dovremo usare una funzione (in C
la funzione risponde al simbolo "+") che prende il primo numero, prende il secondo numero e restituisce in una variabile la loro somma. Lo immaginavate, vero? OK, allora facciamo io abbia dichiarato
tre variabili: int addendo1, addendo2, somma; Adesso ammettiamo che in qualche modo io abbia assegnato ad addendo1 il valore 2, ad addendo2 il valore 3 ed li abbia
sommati mettendo il risultato nella variabile somma: somma = addendo1 + addendo2; Alla fine del
tutto avrò tre variabili in memoria di cui una conterrà un valore interessante (somma) e due che non servono più ma sono ancora piene. Questo avviene indipendentemente dal
processo usato per arrivare alla somma, avrò comunque tre variabili piene e una sola che m'interessa. Certo, avremmo potuto assegnare la somma ad una delle due variabili addendo, come in:
addendo1 = addendo1 + addendo2; Così alla fine avremmo solo due variabili, di cui una utile. Notate che
finora non abbiamo sentito molto la mancanza della nozione "puntatore"... Ora c'è un fatto. Se a noi per un motivo che non sto qui a discutere non piacesse come la funzione somma del C calcola il suo
risultato, potremmo decidere di scrivere noi una funzione di nome addizione che calcola la somma di due numeri e la restituisce in una variabile. Immagino che una funzione di questo tipo avrebbe una
forma simile a: int addizione (int numero1, int numero2); Potremmo chiedere all'utente il valore dei due numeri da sommare,
metterli nelle solite variabili addendo1 e addendo2, poi chiamare la funzione che ho descritto per sommarli. Accadrebbe, accade credetemi, che per fare questa somma benedetta io usi ad
un certo punto cinque variabili di cui quattro uguali a due a due. Quando chiamo la mia funzione, infatti, la macchina non le passa il valore contentuto in addendo1 e addendo2, ma una
loro copia, cosa che in quel momento crea uno spreco di memoria del 100%, ovvero ne serve il doppio per fare la stessa cosa. Alcuni hanno giustamente sottolineato che l'esempio fallisce, poiché
qualtitativamente potrebbe persino accadere che la dimensione dei puntatori sia maggiore della dimensione dei numeri in gioco, e che usare i puntatori in casi come questo sia persino penalizzante. In effetti
l'esempio vuole introdurre un concetto: chiamare una funzione passandogli dei valori, provoca la copia di questi valori. Ora immaginate di passare come valori anziché due piccoli numeri interi due
immagini, ad esempio per ottenere che una divenga la filigrana (o watermark) dell'altra. In un caso come questo, senza troppo ragionare, si capisce che in un dato istante (al momento in cui chiamo la funzione)
avrei in memoria (o peggio ancora in swap...) quattro immagini, uguali a due a due. E che cacchio, non si può evitare di sprecare tutta questa memoria? Come no, usando i puntatori. Usandoli, posso dire
alla funzione di andarsi a leggere i valori dei paramteri che le occorrono al loro indirizzo. Capito il trucco?!

Fico, non sembrano tanto antipatici
No, infatti, immaginate appunto quando invece di passare ad una funzione due numeretti, gli passate delle strutture di memoria belle grandi e complesse, ed immaginate quanto spazio e tempo risparmiate, passando sempre un
piccolo indirizzo, invece di tutta un'enciclopedia di codice binario. E' vero infatto che i puntatori sono variabili, ma è vero anche chi i puntatori sono variabili "piccole". Cosa voglio dire?
Mettiamo che un computer abbia la possibilità di trattare gli indirizzi a 64bit, ovvero che un indirizzo di memoria sia un numero che espresso in binario occupi al massimo 64 cifre binarie. 64 bit sono 8
byte. Da questo discende che un puntatore a qulunque oggetto occupa al massimo quella dimensione. Alcuni dati, come le immagini, raggiungono velocemente dimensioni nell'ordine dei MBytes, ed un MByte sono
già 1000 Kbyte. Se io passassi ad una funzione la copia di una immagine, potrei passarle qualche MByte di dati (mettiamo anche solo un MByte), se le passo il suo indirizzo in memoria le passo al massimo (ne sono certo) 8 byte. C'è una differenza di
125.000 volte. Non si tratta quindi solo di contare il numero delle variabili, si tratta di pesarle. Non solo, se tratto l'immagine con un filtro e l'immagine dopo il trattamento è
raddoppiata di dimensione, il suo puntatore è sempre lo stesso quanto a dimensione. Ma da dove deriva allora l'antipatia? Vi servo subito, e cominciamo da come si dichiarano i puntatori in C (ed
in C++): int *pippo; Ecco qui. Delusi? vi aspettavate una cosa molto complicata? Beh, un po' lo è. Provate a leggere quello che ho scritto. Vediamo... int asterisco pippo
puntevvirgola? E che vuol dire? Eh, ve lo ho detto la cattiveria... Quella scritta si legge "pippo è un puntatore ad un intero". Beh, non sembra difficile... Si, ma quale intero?
Ah... Infatti pippo, preso così non è che faccia molto, ed in effetti per il momento si è solo presentato, ha solo detto "sono pippo, un puntatore ad un numero
intero". Per sapere a cosa "punta" pippo, dobbiamo assegnarlo (quella di prima è una dichiarazione, notate che non c'è l'uguale?), assegnarlo a cosa? Ad un indirizzo, mi sembra
ovvio, sennò che puntatore sarebbe? Mettiamo che abbiamo già dichiarato (ed assegnato): int addendo1 = 2; Scrivere: pippo = &addendo1; Significa appunto dire
che pippo (un puntatore ad intero) contiene l'indirizzo della variabile addendo1, o più brevemente possiamo dire che pippo punta addendo1. A
questo punto sappiamo che:
- pippo è un puntatore ad un (tipo) intero
- addendo1 contiene il numero 2
- pippo contiene l'indirizzo di memoria di addendo1
Adesso arriva la parte violenta, sarebbe meglio che metteste i bambini a letto Se chiedo a pippo cosa contiene, la risposta è scontata: un indirizzo di memoria. Se io vedo pippo nel codice e magari so
che è un puntatore perché ho visto che è stato dichiarato come puntatore, e mi domandassi a quale valore stia puntando in questo momento pippo, glielo devo chiedere, o meglio
glielo chiedo e lo stampo a video, così lo posso leggere. Se non fosse chiaro sto chiedendo a pippo: "qual è il valore contenuto all'indirizzo cui stai puntando adesso?"; insomma voglio che lui mi
dica "2" tanto per dirsela tutta. Beh, la domanda suona pressappoco così: valore = *pippo; Ricorda qualcosa? E certo che ricorda qualcosa, ricorda il modo in cui pippo è stato
dichiarato! Ah, si?! Leggetelo. "pippo è un puntatore ad un uguale valore"?! No, infatti questa non è una dichiarazione, è una assegnazione (c'è l'uguale), e quindi "asterisco
pippo" non ci dice che pippo è un puntatore, ma ci dice che il valore contenuto alla posizione di memoria che pippo sta puntando da quando lo conosciamo, ovvero il numero 2 lo vogliamo mettere in una
variabile di nome valore. In pratica ecco la vera cattiveria, usare la stessa notazione ("asterisco pippo") per fare due cose diverse:
int *pippo; ovvero pippo è un puntatore ad un (tipo) intero
valore = *pippo; ovvero il dato il cui indirizzo è contenuto da pippo il puntatore
Isomma:
int addendo1 = 2;
int *pippo;
pippo = &addendo1;
printf ("%d", *pippo);
printf ("%p", pippo);
Perché tutto fosse più chiaro sarebbe bastato usare un terzo simbolo, come @ (at o chiocciola), per identificare il terzo membro della famiglia, e avremmo avuto uno strumento potente che
non rompe l'anima a nessuno, guardate:
int *pippo;
pippo = &addendo1;
printf ("%d", @pippo);
Prometto, se un giorno dovessi scrivere un linguaggio, userei tre simboli per queste tre cose, i primi due presi dal C e il terzo, diverso dai primi due, per non farvi perdere tempo. Ma ormai il danno è
fatto, ed il C ed il C++ usano solo * e & per tutti e tre ed allora imparate una regoletta empirica ma efficace:
- Se in una espressione compare
&, si sta parlando di un indirizzo di memoria
- Se in una espressione compare
= seguito da un * e dal nome di una variabile si sta cercando il valore cui un puntatore si riferisce
- Se in una espressione, che non sia un assegnamento, compare
* si sta probabilmente dichiarando un puntatore
Tanto per dirsela tutta, in una espressione che non sia una dichiarazione, * e & producono risultati opposti, ovvero l'uno ci da un valore e l'altro l'indirizzo di quel valore. Quindi se proprio
dovessimo usare un terzo simbolo, dovremmo metterlo nella dichiarazione di un puntatore, che ne dite?
Bibliografia & Links
|