
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”.
“Aaah, 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; // somma contiene il risultato di 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; // addendo1 contiene il risultato di 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); // prototipo della funzione addizione
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 contenuto 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é quantitativamente 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 parametri 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 infatti 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 qualunque oggetto occupa al massimo quella dimensione.
Alcuni dati, come le immagini, raggiungono velocemente dimensioni nell’ordine dei MByte, 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; // la variabile intera addendo1 è uguale a due
int* pippo; // pippo è un puntatore ad un (tipo) intero
pippo = &addendo1; // pippo punta all'indirizzo di addendo1
printf ("%d", *pippo); // stampa il numero 2
printf ("%p", pippo); /* stampa un numeraccio strano che probabilmente inizia per 0x
e segue con una serie di numeri, che sono l'indirizzo di memoria a cui sta puntando 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 è un puntatore ad un (tipo) intero
pippo = &addendo1; // pippo punta all'indirizzo di addendo1
printf ("%d", @pippo); // stampa il valore puntato da 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 dà un valore e l’altro l’indirizzo di quel valore
Mi piace:
Mi piace Caricamento...