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
-O0e-O2.
"Sembra funzionare" è il caso pericoloso, perché significa che il bug finisce in produzione. Tre difese:
- Cicla con
i < n. La maggior parte degli sforamenti sono errori di uno nella condizione di un ciclo. - Valida gli indici che arrivano dall'input.
- Compila con un sanitizer mentre sviluppi.
gcc -fsanitize=address -g prog.ctrasforma 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.