In C array e puntatori sono cose diverse che per la maggior parte del tempo si comportano allo stesso modo. È proprio quel "per la maggior parte del tempo" a rendere l'argomento scivoloso: il codice che tratta un array come un puntatore di solito funziona, fino al giorno in cui finisce in uno dei due punti in cui la differenza conta e la risposta è sbagliata senza che nessuno se ne accorga.
Questa pagina traccia il confine con precisione.
Il decadimento dell'array: l'unica regola dietro a tutto
In quasi ogni espressione, il nome di un array viene convertito automaticamente in un puntatore al suo primo elemento. Questo si chiama decadimento (decay). arr diventa &arr[0], con tipo int *.
Il nome dell'array e l'indirizzo del suo primo elemento vengono stampati identici. È il decadimento in azione, ed è per questo che puoi assegnare un array a un puntatore senza &.
Ci sono esattamente tre casi in cui il decadimento non avviene: come operando di sizeof, come operando di & e quando inizializzi un array di caratteri con un letterale stringa. In tutti gli altri casi, dai per scontato il decadimento.
arr[i] è letteralmente *(arr + i)
Lo standard C definisce l'operatore di indicizzazione in termini di aritmetica dei puntatori: a[b] significa *(a + b). L'indicizzazione non è una funzionalità a parte: è un'abbreviazione.
Ne derivano due conseguenze. Primo, un puntatore si può indicizzare: p[2] è *(p + 2), ed è per questo che le funzioni che ricevono un puntatore possono comunque usare la familiare sintassi arr[i]. Secondo, 2[arr] compila: *(2 + arr) è lo stesso indirizzo di *(arr + 2). Nessuno lo scrive fuori da un quiz, ma chiude la questione se l'indicizzazione sia "davvero" aritmetica dei puntatori.
Passare un array a una funzione
A causa del decadimento, una funzione non riceve mai un array. Riceve un puntatore.
Tre cose da ricavare da quell'esempio.
La dimensione in int arr[100] è documentazione, niente di più. Il compilatore riscrive il parametro come int *arr e non controlla mai che tu abbia passato 100 di qualcosa.
La lunghezza deve viaggiare a parte. La funzione ha un indirizzo e nessuna idea di quanto si estendano i dati. È per questo che praticamente ogni funzione C che accetta un array accetta anche un conteggio: memcpy(dst, src, n), fread(ptr, size, count, f), qsort(base, nmemb, size, cmp). Le stringhe fanno eccezione solo perché portano con sé il proprio marcatore di fine, il '\0'.
Passare un puntatore costa poco e i dati sono condivisi. Non viene fatta nessuna copia dell'array, quindi la funzione può modificare gli elementi del chiamante, ed è spesso proprio quello che vuoi:
Marca i parametri in sola lettura come const int *, come fa print_all. Documenta l'intenzione e permette al compilatore di intercettare una scrittura accidentale.
La trappola di sizeof
Ecco il bug che colpisce ogni programmatore C almeno una volta. sizeof è uno dei due casi in cui il decadimento non avviene, quindi restituisce la vera dimensione dell'array dove l'array è dichiarato, e la dimensione di un puntatore ovunque l'array sia decaduto.
In main, sizeof data vale 40 byte e la lunghezza risulta 10. Dentro inspect, sizeof arr è la dimensione di un puntatore, 8 su una macchina a 64 bit, e la "lunghezza" risulta 2. Il codice sembra identico ed è sbagliato senza dare nell'occhio.
La regola che ne segue: calcola la lunghezza di un array solo nello scope in cui l'array è stato dichiarato, e passala da lì in poi.
& di un array non ha lo stesso tipo
L'altro caso in cui il decadimento non avviene è l'operatore &. &arr è un puntatore all'intero array, non al suo primo elemento. Entrambi contengono lo stesso indirizzo; la differenza è cosa significa "aggiungi 1".
Stesso indirizzo di partenza, passo diverso. arr ha tipo int * e avanza di un elemento; &arr ha tipo int (*)[5] e avanza dell'intero array. È il meccanismo che fa funzionare il passaggio di una riga di un array 2D, trattato negli array multidimensionali.
Puntatore a un array e array di puntatori
Due dichiarazioni che si somigliano e significano cose opposte:
int *p[5]; // array di 5 puntatori a int
int (*q)[5]; // puntatore a un array di 5 int
[] ha una precedenza più alta di *, quindi int *p[5] si legge come "p è un array, di puntatori". Le parentesi in int (*q)[5] costringono * a legarsi per primo: "q è un puntatore, a un array".
int *p[5] int (*q)[5]
p[0] -> [ int ] q -> [ int | int | int | int | int ]
p[1] -> [ int ] un unico blocco contiguo di 5
p[2] -> [ int ]
p[3] -> [ int ]
p[4] -> [ int ]
cinque indirizzi separati
L'array di puntatori è quello comune, e l'hai già usato: char *argv[] è un array di puntatori a stringhe, uno per ogni argomento da riga di comando. È anche il modo per tenere una lista di stringhe di lunghezze diverse senza sprecare spazio.
Nota (*q)[0]: dereferenzia il puntatore per ottenere l'array, poi indicizzalo. Anche qui le parentesi sono obbligatorie, per lo stesso motivo di precedenza.
Dove array e puntatori differiscono davvero
Tieni a mente questa breve tabella e la confusione sparisce:
Array int arr[10] | Puntatore int *p | |
|---|---|---|
| Cos'è | 10 int disposti in memoria | una variabile che contiene un indirizzo |
sizeof | 40: l'intero blocco | 8: solo il puntatore |
| Assegnabile | no: arr = x; è un errore | sì: p = x; va bene |
& restituisce | int (*)[10] | int ** |
| Da dove viene la memoria | dichiarata insieme all'array | ovunque tu lo faccia puntare |
Il nome di un array non è una variabile che contiene un indirizzo: è la memoria stessa, e l'indirizzo viene calcolato quando serve. Ecco perché non puoi assegnargli un valore.
Domande frequenti
In C array e puntatori sono la stessa cosa?
No, ma è facile confonderli perché il nome di un array decade in un puntatore al suo primo elemento in quasi ogni espressione. L'array in sé è un blocco di elementi con una dimensione nota; un puntatore è una singola variabile che contiene un indirizzo. sizeof e & sono i due punti in cui la differenza si vede.
Perché sizeof(arr) dà la risposta sbagliata dentro una funzione?
Perché il parametro non è un array. void f(int arr[]) viene riscritto in silenzio come void f(int *arr), quindi sizeof(arr) misura un puntatore (8 byte sulla maggior parte delle macchine), non l'array originale. Passa la lunghezza come parametro separato.
Qual è la differenza tra int *p[5] e int (*p)[5]?
int *p[5] è un array di 5 puntatori a int. int (*p)[5] è un singolo puntatore a un array di 5 int. Le parentesi legano prima * al nome; senza di esse vince [], che ha una precedenza più alta.
arr[i] è davvero la stessa cosa di *(arr + i)?
Sì: lo standard definisce a[b] come *(a + b). È anche il motivo per cui lo strano i[arr] compila e funziona: l'addizione è commutativa, quindi *(i + arr) è lo stesso elemento. Non scriverlo mai nel codice vero, ma dimostra la regola.