Menu

Puntatori in C: guida completa con esempi

Un puntatore è una variabile che memorizza un indirizzo di memoria. Questa pagina costruisce l'idea da zero: gli operatori & e *, dichiarare e dereferenziare i puntatori, perché il tipo del puntatore conta e la funzione swap() che mostra perché i puntatori esistono.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

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 trova x.
  • *p: dereferenziazione. Va all'indirizzo contenuto in p e 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:

  1. Quanti byte leggere. Dereferenziare un int * legge 4 byte (di solito); dereferenziare un double * ne legge 8. Senza il tipo, *p non avrebbe senso.
  2. Come interpretare quei byte. Lo stesso schema di bit è un numero come int e uno completamente diverso come float.

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.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA