Menu

Array in C: dichiarazione, inizializzazione e cicli

Come dichiarare e inizializzare gli array in C, indicizzarli da zero, calcolarne la lunghezza con sizeof, scorrere gli elementi e perché leggere oltre la fine è comportamento indefinito e non un messaggio di errore.

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

Un array è un blocco di dimensione fissa di elementi dello stesso tipo, disposti uno dopo l'altro in memoria. Tutto dipende da questa disposizione contigua: è il motivo per cui l'accesso tramite indice è immediato, per cui array e puntatori sono così legati e per cui C può darti l'indirizzo dell'elemento zero e lasciarti trovare gli altri con l'aritmetica.

Dichiarare un array

Tipo degli elementi, nome e una dimensione tra parentesi quadre:

int    scores[5];       // 5 ints
double prices[100];     // 100 doubles
char   initials[3];     // 3 chars

La dimensione è il numero di elementi e deve essere nota nel punto in cui l'array viene dichiarato: di solito è un letterale o una costante definita con #define. La memoria viene riservata subito, non c'è nessuna allocazione successiva.

Un array locale appena dichiarato contiene spazzatura, non zeri:

La seconda riga può stampare zeri, numeri enormi, e può cambiare tra un'esecuzione e l'altra o tra una build di debug e una di release. Leggere quei valori è comportamento indefinito: inizializza prima di leggere.

Inizializzazione

Diverse forme, tutte utili:

L'idioma da ricordare è {0}: tutti gli elementi che non elenchi vengono inizializzati a zero, quindi un solo zero azzera l'intero array. Gli inizializzatori designati ([4] = 99) ti permettono di impostare posizioni specifiche e lasciare il resto a zero, comodo per tabelle di lookup quasi vuote.

Una cosa che non puoi fare è assegnare un array dopo la dichiarazione:

int a[3] = {1, 2, 3};
int b[3];
b = a;                    // errore: un array non è assegnabile
memcpy(b, a, sizeof a);   // così si copia un array

Gli indici partono da zero

Il primo elemento è arr[0] e l'ultimo è arr[n-1]. Non esiste arr[n].

Esegui l'ultimo ciclo e guarda gli indirizzi: ognuno si trova esattamente sizeof(int) byte dopo il precedente. Gli elementi sono davvero disposti uno dopo l'altro:

 indice     0         1         2         3         4
         +--------+--------+--------+--------+--------+
 valore  |   88   |   95   |   75   |   60   |  100   |
         +--------+--------+--------+--------+--------+
 offset   +0       +4       +8       +12      +16      byte
          ^
          &scores[0], che è anche ciò a cui `scores` decade

L'indicizzazione da zero non è una scelta arbitraria: rende l'indice un offset dall'inizio, che è proprio ciò che serve all'aritmetica degli indirizzi. scores[i] è definito come *(scores + i); vedi puntatori e array.

Trovare la lunghezza

C non memorizza da nessuna parte la lunghezza di un array a runtime, ma il compilatore la conosce e sizeof ti dà accesso a questa informazione:

L'idioma è sizeof(arr) / sizeof(arr[0]). Dividere per arr[0] invece che per un sizeof(double) scritto a mano fa sì che la riga continui a funzionare se cambi il tipo degli elementi.

La trappola: funziona solo dove l'array è stato dichiarato. Passa l'array a una funzione e il parametro diventa un puntatore, quindi sizeof misura il puntatore: di solito 8 byte, che danno una "lunghezza" di 1 o 2. Ecco perché ogni funzione C che riceve un array riceve anche un conteggio:

Scorrere un array con un ciclo

La forma standard è un ciclo for che parte da 0 e continua finché i < n:

Nota i < n, non i <= n. Con n elementi gli indici validi vanno da 0 a n - 1, quindi <= esegue un passaggio in più e legge oltre la fine. Inizializzare hottest e coldest con temps[0] invece che con 0 è l'altra abitudine da copiare: far partire un massimo da zero si rompe in silenzio con dati tutti negativi.

Uscire dai limiti è comportamento indefinito

Questa è la parte di C che sorprende chi arriva da altri linguaggi. Non c'è alcun controllo dei limiti. Né in compilazione né a runtime. arr[10] su un array di cinque elementi non è un errore: è un calcolo di indirizzo che il compilatore esegue senza commenti.

int arr[5] = {1, 2, 3, 4, 5};
arr[7] = 99;     // scrive 8 byte oltre la fine dell'array
int x = arr[-1]; // legge prima dell'inizio

Quello che succede dopo è comportamento indefinito, e i sintomi sono vari in modo poco utile:

  • sembra funzionare, perché la memoria colpita non era usata;
  • un'altra variabile cambia senza un motivo visibile;
  • il programma va in crash con un segmentation fault, magari molto più tardi;
  • il comportamento cambia tra -O0 e -O2.

"Sembra funzionare" è il caso pericoloso, perché significa che il bug finisce in produzione. Tre difese:

  1. Cicla con i < n. La maggior parte degli sforamenti sono errori di uno nella condizione di un ciclo.
  2. Valida gli indici che arrivano dall'input.
  1. Compila con un sanitizer mentre sviluppi. gcc -fsanitize=address -g prog.c trasforma la maggior parte degli sforamenti in una segnalazione immediata che indica il file, la riga e l'array coinvolto.

Array di altri tipi

La stessa sintassi funziona con qualsiasi tipo di elemento, comprese le struct:

Vale la pena soffermarsi sulla riga char word[6] = "hello";: una stringa in C è semplicemente un array di char il cui ultimo byte significativo è '\0'. Quel terminatore è il motivo per cui l'array ha bisogno di sei posizioni per cinque lettere.

Array a lunghezza variabile, in breve

C99 permette un array la cui dimensione è un valore a runtime:

int n = get_count();
int buffer[n];        // un array a lunghezza variabile

Due avvertenze. La dimensione è fissa una volta creato l'array, quindi un VLA non è una lista che può crescere. E la memoria arriva dallo stack, quindi un n grande o controllato da un attaccante può farlo traboccare e mandare in crash il programma: è per questo che molti progetti, compreso il kernel Linux, vietano del tutto i VLA. Inoltre in C11 il loro supporto è facoltativo per le implementazioni.

Quando la dimensione è davvero sconosciuta fino al runtime, alloca la memoria:

Nota che arr[i] si legge allo stesso modo sia che arr sia un array sia che sia un puntatore a memoria allocata: la sintassi di indicizzazione non fa differenza. Tutti i dettagli nella memoria dinamica.

Domande frequenti

Come si dichiara un array in C?

Indica il tipo degli elementi, un nome e una dimensione tra parentesi quadre: int scores[5]; riserva spazio per cinque int. La dimensione deve essere una costante nota nel punto in cui dichiari l'array (a parte gli array a lunghezza variabile) e gli elementi partono non inizializzati, a meno che tu non fornisca dei valori.

Come si trova la lunghezza di un array in C?

sizeof(arr) / sizeof(arr[0]): la dimensione totale in byte divisa per la dimensione di un elemento. Funziona solo nello scope in cui l'array è stato dichiarato: una volta passato a una funzione, l'array è decaduto a puntatore e sizeof misura il puntatore.

Cosa succede se vai oltre la fine di un array in C?

Niente ti ferma. C non controlla i limiti, quindi arr[10] su un array di 5 elementi legge o scrive qualunque cosa si trovi in quella memoria. È comportamento indefinito: può stampare valori spazzatura, corrompere un'altra variabile o mandare in crash il programma, e può sembrare funzionare finché non smette di farlo.

Come si inizializzano a zero tutti gli elementi di un array in C?

int arr[100] = {0};: tutti gli elementi che non elenchi vengono inizializzati a zero, quindi un solo zero azzera l'intero array. In C23 funziona anche int arr[100] = {};. Senza alcun inizializzatore, il contenuto di un array locale è spazzatura indeterminata.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA