Una fila di valori a dimensione fissa
Un array è un blocco contiguo di memoria che contiene un numero fisso di valori dello stesso tipo. Dove un singolo int memorizza un numero, un array di int ne memorizza molti uno dopo l'altro, ciascuno raggiungibile tramite un indice intero.
Lo dichiari indicando il tipo degli elementi, un nome e una dimensione tra parentesi quadre. Aggiungi una lista tra graffe per riempirlo:
Gli indici partono da zero: il primo elemento è scores[0], e un array di dimensione 4 ha indici validi da 0 a 3. La dimensione deve essere una costante nota in fase di compilazione: in C++ standard non puoi scrivere int n = readInput(); int a[n]; (è un'estensione non portabile). Quando ti serve una dimensione decisa a runtime, usa invece un vector.
Inizializzare gli array
Hai diversi modi per riempire un array, e un paio di scorciatoie che vale la pena conoscere:
L'insidia da ricordare: quando fornisci meno inizializzatori della dimensione, gli elementi rimanenti vengono inizializzati per valore (zero per i tipi numerici), non lasciati con valori casuali. Ma un array senza alcun inizializzatore, come int e[4]; dichiarato come variabile locale, contiene valori indeterminati, e leggerli prima di assegnarli è comportamento indefinito.
Scorrere un array
Dato che gli elementi sono memorizzati in modo contiguo, scorri un array con un normale ciclo sugli indici. Per restare nei limiti, guida il ciclo con la lunghezza reale dell'array invece che con un numero scritto a mano:
sizeof(scores) è il numero totale di byte dell'intero array; dividendolo per sizeof(scores[0]) (la dimensione di un elemento) ottieni il numero di elementi. Da C++17 esiste una forma più pulita, std::size(scores), che si legge meglio e si rifiuta di compilare se per errore le passi un puntatore. Ancora più semplice quando ti servono solo i valori: un for basato su range evita del tutto i calcoli sugli indici.
La trappola dei limiti
Il C++ non controlla i limiti di arr[i]. Usare un indice oltre l'ultimo elemento non lancia eccezioni né avvisi: legge o scrive qualunque cosa si trovi in quella memoria. È il bug più comune con gli array ed è un classico caso di comportamento indefinito:
int a[3] = {1, 2, 3};
a[3] = 99; // OOPS - valid indices are 0..2, not 3
cout << a[5]; // garbage, crash, or corruption - undefined behavior
L'errore di uno (off-by-one) di solito si nasconde nella condizione di un ciclo. Scrivere i <= n invece di i < n fa un passo di troppo e tocca arr[n], che non esiste:
for (int i = 0; i <= n; i++) // BUG: when i == n, arr[i] is out of bounds
cout << arr[i];
La soluzione è la disciplina della sezione precedente: cicla con i < size, mai con <=, e calcola la dimensione dall'array invece di riscrivere un valore letterale che smette di essere corretto quando aggiungi un elemento.
Decadimento degli array: il puntatore nascosto
Il comportamento più subdolo degli array in C++ è il decadimento (decay): quando passi un array a una funzione, viene convertito in silenzio in un puntatore al suo primo elemento. L'informazione sulla dimensione va persa, quindi sizeof dentro la funzione misura il puntatore, non l'array.
Nota che int arr[] e int* arr sono identici come parametri di funzione: le parentesi quadre sono solo estetiche. Dato che il conteggio è perso, devi passare tu la lunghezza insieme all'array:
int sum(const int* arr, int n) {
int total = 0;
for (int i = 0; i < n; i++) total += arr[i];
return total;
}
Questo schema "passa un puntatore e una lunghezza e spera che corrispondano" è proprio l'attrito che spinge la maggior parte del codice C++ verso std::array e std::vector, che portano con sé la propria dimensione e non decadono mai.
Due parole sugli array multidimensionali
Puoi annidare le parentesi quadre per creare una griglia. Un array 2D è in realtà un array di array, disposto in memoria riga per riga:
Usalo con grid[row][col]. Valgono le stesse avvertenze su limiti e decadimento, e anzi peggiorano, perché passare un array 2D a una funzione richiede di specificare ogni dimensione tranne la prima (void f(int g[][3])). Per tutto ciò che va oltre una piccola griglia fissa, un vector di vector è molto meno soggetto a errori.
Prossimo: Vector
Gli array classici sono veloci e prevedibili, ma la dimensione fissa, l'assenza di controlli sui limiti e il decadimento a puntatore li rendono scomodi nel codice di tutti i giorni. Nella prossima pagina incontrerai std::vector, un array ridimensionabile che cresce su richiesta, ricorda la propria dimensione e si collega direttamente agli algoritmi della STL, dandoti quasi tutto ciò che offrono gli array con molti meno modi per spararti sui piedi.
Domande frequenti
Come si dichiara e si inizializza un array in C++?
Scrivi il tipo degli elementi, un nome e la dimensione tra parentesi quadre, poi facoltativamente una lista tra graffe: int scores[4] = {90, 75, 100, 60};. Puoi omettere la dimensione quando fornisci gli inizializzatori: int scores[] = {90, 75, 100, 60}; lascia che sia il compilatore a contarli per te.
Come si ottiene la lunghezza di un array in C++?
Per un vero array ancora nello scope, usa std::size(arr) (C++17) oppure sizeof(arr) / sizeof(arr[0]). Questo non funziona più quando l'array è decaduto a puntatore (per esempio dentro una funzione che riceve int arr[]), perché lì sizeof restituisce la dimensione del puntatore, non dell'array.
Cosa succede se accedi a un array fuori dai limiti in C++?
È comportamento indefinito. Il C++ non controlla i limiti di arr[i], quindi leggere o scrivere oltre la fine può far crashare il programma, restituire valori spazzatura o corrompere in silenzio la memoria vicina. Tieni sempre l'indice nell'intervallo da 0 a size - 1.