Ogni variabile del tuo programma vive da qualche parte in memoria, e ogni "da qualche parte" ha un numero: il suo indirizzo. Un puntatore è semplicemente una variabile che memorizza uno di questi numeri. Tutta l'idea è questa. Il resto della pagina è sintassi e conseguenze.
Il motivo per cui i puntatori contano così tanto in C è che il C passa a ogni funzione una copia dei suoi argomenti. Una funzione che riceve il numero 7 può cambiare la sua copia quanto vuole; la variabile del chiamante non si sposta. Passa invece alla funzione un indirizzo, e questa potrà risalire e cambiare l'originale. Array, stringhe, memoria dinamica e ogni struttura dati che costruirai mai in C poggiano su quest'unica capacità.
Memoria, indirizzi e perché tutto ne ha uno
Quando scrivi int score = 42;, il compilatore riserva qualche byte di memoria e gli dà un nome che puoi usare. Quei byte hanno anche un indirizzo numerico. Immagina la memoria come una strada lunghissima di case numerate: score è la famiglia che abita al civico 0x7ffd4c2a. Il nome serve a te, il numero alla macchina.
L'operatore & chiede quel numero:
Eseguilo. I tre indirizzi sono diversi perché le tre variabili occupano byte diversi, e cambieranno da un'esecuzione all'altra: è normale, ed è il motivo per cui non si scrive mai un indirizzo fisso nel codice. Nota il cast (void *): %p è specificato per ricevere un void *, e passare qualsiasi altro tipo di puntatore è tecnicamente comportamento indefinito. Fai del cast un'abitudine.
Che gli indirizzi siano imprevedibili non è un problema, perché non ti interessa quasi mai quale sia un indirizzo. Ti interessa averlo.
Dichiarare un puntatore
La dichiarazione di un puntatore indica il tipo a cui punta:
int *p; // p puo' contenere l'indirizzo di un int
double *q; // q puo' contenere l'indirizzo di un double
char *name; // name puo' contenere l'indirizzo di un char
Leggi int *p; come "*p è un int": dereferenziare p ti dà un int. Questa lettura regge anche con tutte le dichiarazioni complicate che il C ti proporrà più avanti.
Dove metti l'* è una scelta di stile che il compilatore ignora: int* p, int *p e int * p dichiarano tutti la stessa cosa. La maggior parte del codice C scrive int *p, e c'è un motivo pratico:
int* a, b; // a e' un puntatore a int; b e' un int NORMALE, non un puntatore
int *a, *b; // entrambi sono puntatori: l'* si lega al nome, non al tipo
L'* si attacca al dichiaratore, non al tipo. Scriverlo accanto al nome lo mantiene visibile.
& e *: i due operatori
Due operatori fanno tutto il lavoro, e sono esattamente l'uno l'opposto dell'altro.
&x: indirizzo di. Produce l'indirizzo in cui si trovax.*p: dereferenziazione. Va all'indirizzo contenuto inpe ti dà l'oggetto che si trova lì.
Le ultime due righe sono tutto il senso dei puntatori. Niente ha assegnato direttamente un valore ad age, eppure age è cambiata, perché *p = 31 significa "memorizza 31 all'indirizzo contenuto in p", e quell'indirizzo è quello di age.
Ecco l'immagine in memoria:
p age
+------------+ +----------+
| 0x7ffd1c40 | --------> | 31 |
+------------+ +----------+
in 0x7ffd1c38 in 0x7ffd1c40
p -> l'indirizzo 0x7ffd1c40
*p -> l'int memorizzato li', 31
&age -> 0x7ffd1c40, lo stesso indirizzo contenuto in p
&p -> 0x7ffd1c38, dove si trova p stesso
Nota l'ultima riga: il puntatore è a sua volta una variabile da qualche parte in memoria, quindi ha anche lui un indirizzo. Non è una domanda a trabocchetto, è solo coerenza, ed è così che funzionano i puntatori a puntatori (int **).
Dato che & e * si annullano a vicenda, *&age è semplicemente age, e &*p è semplicemente p.
Perché il tipo del puntatore conta
Un puntatore memorizza un indirizzo, e un indirizzo è solo un numero: allora perché il compilatore insiste a sapere se punta a un int o a un double? Due motivi:
- Quanti byte leggere. Dereferenziare un
int *legge 4 byte (di solito); dereferenziare undouble *ne legge 8. Senza il tipo,*pnon avrebbe senso. - Come interpretare quei byte. Lo stesso schema di bit è un numero come
inte uno completamente diverso comefloat.
La dimensione del puntatore in sé, invece, non dipende da ciò a cui punta: un indirizzo è un indirizzo.
Su una macchina a 64 bit tutti e tre i puntatori occupano 8 byte, mentre le cose a cui puntano ne occupano 4, 8 e 1. È il tipo puntato a dare un significato a *p e all'aritmetica dei puntatori.
Assegnare tra tipi di puntatore diversi è un errore di cui il compilatore si lamenterà, e a ragione: pi = &d; ti preparerebbe a leggere 4 byte di un double e a chiamare il risultato un intero.
La motivazione classica: swap()
Ecco la funzione che ogni corso di C tira fuori, perché fallisce esattamente nel modo che spiega i puntatori.
La funzione dichiara di averli scambiati, e main dichiara che non è successo nulla. broken_swap ha ricevuto copie di x e y. Ha scambiato perfettamente le sue copie, poi quelle copie hanno smesso di esistere quando la funzione è terminata.
Passale invece gli indirizzi:
Ora a e b sono ancora copie, copie di due indirizzi, ma la copia di un indirizzo punta alla stessa identica variabile a cui puntava l'originale. *a è x, da qualunque punto venga chiamata la funzione.
È lo schema dietro ogni funzione C che modifica i dati del chiamante, e dietro i parametri delle funzioni in generale: il C non ha il passaggio per riferimento, quindi passi un puntatore e lo dereferenzi. È anche il motivo per cui scanf ha bisogno di una &:
int n;
scanf("%d", &n); // scanf deve poter scrivere in n, quindi le serve l'indirizzo di n
Restituire più di un valore
Una funzione C restituisce un solo valore. I puntatori sono il modo per aggirare il limite: passa gli indirizzi per i risultati aggiuntivi.
Il valore di ritorno porta lo stato e i puntatori portano i risultati. Questa divisione si trova ovunque nella libreria standard del C e nelle API dei sistemi operativi.
Puntatori a puntatori
Dato che un puntatore è una variabile, puoi prenderne l'indirizzo, e il tipo di quell'indirizzo è "puntatore a puntatore a int":
pp p value
+------+ +------+ +------+
| &p | ----> | &val | -----> | 8 |
+------+ +------+ +------+
Incontrerai ** sul serio quando una funzione deve cambiare un puntatore che appartiene al chiamante, per esempio un allocatore che imposta il puntatore del chiamante su memoria nuova, e nel char *argv[] degli argomenti da riga di comando.
Quattro errori da conoscere prima di commetterli
Usare un puntatore che non è mai stato fatto puntare da nessuna parte. Un puntatore non inizializzato contiene i byte che si trovavano già lì. Dereferenziarlo legge o scrive a un indirizzo casuale.
int *p; // p contiene spazzatura
*p = 10; // comportamento indefinito: probabilmente un crash
Inizializza ogni puntatore, con un indirizzo reale o con NULL. Vedi i puntatori nulli per la disciplina che ci ruota intorno.
Confondere *p con p. p = 5; imposta il puntatore all'indirizzo 5 (un'assurdità); *p = 5; memorizza 5 dove punta. Il compilatore avvisa nel primo caso, e gli avvisi vale la pena leggerli.
Dimenticare la & in scanf. scanf("%d", n) passa il valore di n come indirizzo. Compila con un avviso e poi scrive nella memoria indicata da quel numero.
Restituire l'indirizzo di una variabile locale. La variabile locale sparisce nel momento in cui la funzione termina; l'indirizzo è la ricevuta di un edificio già demolito.
int *bad(void) {
int local = 42;
return &local; // il chiamante riceve un puntatore pendente
}
Se ti serve memoria che sopravviva alla chiamata, allocala: è a questo che serve la memoria dinamica.
Dove andare dopo
I puntatori non si fermano a "contenere un indirizzo". Sommare a un puntatore lo fa avanzare in memoria in unità del suo tipo, ed è l'argomento dell'aritmetica dei puntatori; e in C il nome di un array decade in un puntatore al suo primo elemento, ed è per questo che puntatori e array, una volta che li conosci entrambi, sono di fatto lo stesso argomento. Leggi quelle due pagine, e il resto del C smetterà di sembrare magia.
Domande frequenti
Cos'è un puntatore in C?
Un puntatore è una variabile il cui valore è un indirizzo di memoria, cioè la posizione di un altro oggetto in memoria. int *p = &age; fa sì che p contenga l'indirizzo di age, e *p poi legge o scrive la variabile age tramite quell'indirizzo.
Qual è la differenza tra * e & in C?
&x è l'operatore indirizzo: produce l'indirizzo in cui si trova x. *p è l'operatore di dereferenziazione: va all'indirizzo memorizzato in p e ti dà l'oggetto che si trova lì. Si annullano a vicenda, quindi *&x è semplicemente x.
Perché * compare due volte, nella dichiarazione e quando si usa il puntatore?
Sono due usi diversi dello stesso simbolo. In int *p; l'* fa parte del tipo e dice "p è un puntatore a int". In *p = 5; l'* è l'operatore di dereferenziazione e dice "memorizza 5 all'indirizzo contenuto in p". Leggere la dichiarazione come "*p è un int" dà senso a entrambi.
Perché servono i puntatori in C?
Il C passa ogni argomento per valore, quindi una funzione non può mai cambiare una variabile del chiamante a meno che non ne riceva l'indirizzo. I puntatori permettono anche di percorrere gli array in modo efficiente, restituire più di un risultato, costruire strutture collegate e usare la memoria allocata a runtime con malloc.