Menu

Puntatori e array in C: decadimento, arr[i] e la trappola di sizeof

In C il nome di un array diventa un puntatore al suo primo elemento quasi ovunque lo usi. Questa pagina spiega quel decadimento, perché arr[i] è letteralmente *(arr+i), perché la dimensione dell'array deve viaggiare a parte nelle funzioni e in cosa un puntatore a un array differisce da un array di puntatori.

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

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 memoriauna variabile che contiene un indirizzo
sizeof40: l'intero blocco8: solo il puntatore
Assegnabileno: arr = x; è un erroresì: p = x; va bene
& restituisceint (*)[10]int **
Da dove viene la memoriadichiarata insieme all'arrayovunque 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.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA