Array Monodimensionali statici in linguaggio C

Molte applicazioni richiedono il processamento di molti dati che hanno le medesime caratteristiche. Ad esempio un insieme di dati numerici rappresentati da x_1, x_2, \dots x_n. In tali situazioni è molto conveniente piazzare i dati in un array.

Gli array sono una delle strutture dati più importanti nella programmazione. Un array è un insieme di elementi dello stesso tipo che possono essere acceduti tramite un indice. In linguaggio C, gli array possono essere statici o dinamici, ed in questa lezione ci concentreremo sugli array statici monodimensionali.

Gli array statici monodimensionali in C sono dichiarati specificando il tipo di dati degli elementi e il numero di elementi all'interno dell'array. Una volta dichiarato, l'array statico ha una dimensione fissa e non può essere modificata in futuro. Gli elementi di un array sono contigui in memoria e possono essere acceduti utilizzando l'operatore di indice [].

Assegnare valori agli elementi di un array statico in C è semplice: è sufficiente utilizzare l'operatore di assegnazione (=) e specificare l'indice dell'elemento. È anche possibile utilizzare un ciclo per assegnare valori agli elementi dell'array, ad esempio utilizzando un ciclo for.

Accedere agli elementi di un array statico in C è altrettanto semplice: è sufficiente utilizzare l'operatore di indice [] e specificare l'indice dell'elemento. In questa lezione vedremo tutti questi concetti e forniremo esempi pratici per aiutare a capire meglio gli array statici monodimensionali in C.

Concetti Chiave
  • Dichiarazione di un array statico monodimensionale: come specificare il tipo di dati degli elementi e il numero di elementi all'interno dell'array;
  • Assegnazione di valori agli elementi dell'array: come utilizzare l'operatore di assegnazione (=) e gli indici per assegnare valori agli elementi dell'array;
  • Accesso agli elementi dell'array: come utilizzare l'operatore di indice [] per accedere agli elementi dell'array;
  • Utilizzo di cicli per accedere e assegnare valori agli elementi dell'array: come utilizzare cicli, come for, per lavorare con gli elementi dell'array;
  • Memoria contigua: gli elementi di un array sono contigui in memoria, ciò rende efficiente l'accesso;
  • Dimensione fissa: una volta dichiarato, l'array statico ha una dimensione fissa e non può essere modificata in futuro;

Array monodimensionali statici

Un array è una struttura dati che contiene un certo numero di dati i quali sono tutti dello stesso tipo. Questi dati prendono il nome di elementi dell'array e possono essere selezionati in maniera individuale in base alla loro posizione all'interno dell'array.

Il tipo più semplice di array è quello monodimensionale, ossia ad una singola dimensione. Concettualmente, gli elementi di un array di questo tipo sono organizzati, o meglio disposti in memoria, uno adiacente all'altro di seguito a formare una singola riga (o colonna a seconda di come li si visualizza). Ad esempio, un array di nome x di 10 elementi può essere visualizzato in questo modo:

Array Monodimensionale Statico
Figura 1: Array Monodimensionale Statico

Il numero di elementi che un array può contenere al proprio interno può essere fissato a priori oppure cambiare a tempo di esecuzione, ossia cambiare durante l'esecuzione del programma stesso.

Nel primo caso si parla di array statici mentre nel secondo caso di array dinamici.

Per poter lavorare con array dinamici sono richieste conoscenze ulteriori di meccanismi di gestione della memoria e soprattutto è richiesta la conoscenza dei puntatori che vedremo nelle prossime lezioni. Per il momento ci concentreremo sullo studio degli array statici per cui il numero di elementi è fissato a tempo di compilazione.

Definizione

Array Statici

Un'array statico è una struttura dati composta da un numero di elementi omogenei, ossia dello stesso tipo. Tali elementi sono disposti in memoria in modo sequenziale ossia l'uno adiacente all'altro a formare una riga (o colonna) contigua di elementi.

Gli elementi di un array possono essere acceduti utilizzando uno o più indici. Quando l'indice è uno soltanto si parla di Array Monodimensionali.

Un array statico ha un numero di elementi fissato a priori, ossia a tempo di compilazione. Il numero di elementi non può essere modificato dinamicamente.

Dichiarazione di un array statico

Per poter dichiarare un array statico in linguaggio C bisogna specificare sia il tipo degli elementi dell'array, sia il numero di elementi che l'array contiene.

La sintassi generale per far questo è la seguente:

tipo_elementi nome_array[numero_elementi];

Per cui, volendo dichiarare un array di nome x che contiene 10 elementi di tipo int possiamo scrivere in questo modo:

int x[10];

Gli elementi di un array possono essere di un tipo qualunque mentre la lunghezza di un array deve essere sempre specificata utilizzando un'espressione costante che si traduce in un intero positivo. In particolare non è possibile utilizzare un'espressione il cui risultato sia noto soltanto a tempo di esecuzione.

Definizione

Dichiarazione di un Array Statico

La sintassi per dichiarare un array statico è la seguente:

tipo nome[espressione_costante];
  • Il tipo può essere un qualunque tipo valido del linguaggio C;
  • l'espressione espressione_costante deve essere una qualunque valida espressione a valori interi il cui risultato deve essere noto a tempo di compilazione.

Indicizzazione di un array

Per accedere ad un particolare elemento di un array si utilizza l'indicizzazione.

Sintatticamente, si tratta di specificare il nome dell'array seguito da un valore intero racchiuso tra parentesi quadre.

La cosa fondamentale da ricordare è che gli elementi di un array sono numerati a partire da zero, 0 e non da 1. Per cui, se abbiamo un array con n elementi, questi ultimi saranno indicizzati con indici che vanno da 0 ad n - 1.

Ad esempio, supponiamo di dichiarare un array di 10 elementi interi:

int x[10];

Se vogliamo accedere al quinto elemento dobbiamo usare come indice il numero 4, per cui:

printf("Quinto elemento: %d\n", x[4]);

Gli elementi dell'array x saranno indicati dalla sintassi: x[0], x[1], x[2], \dots, x[9].

Per ricordare bene il fatto che gli indici di un array partano da zero bisogna intendere il valore non tanto come un indice ma come uno scarto o offset. Se immaginiamo il nome di un array come l'etichetta di una locazione di memoria (che nella realtà è così) allora il valore tra parentesi quadre rappresenta il numero di caselle di cui bisogna spostarsi per raggiungere l'elemento desiderato.

Per cui, per accedere al primo elemento mi devo spostare di zero caselle rispetto alla testa dell'array e così via.

Espressioni della forma x[i] sono trattate come variabili e possono essere usate come tali. Ad esempio possiamo modificare il valore del primo elemento dell'array x in questo modo:

/* Modifica del primo elemento */
x[0] = 1;

E possiamo richiamare il valore di un elemento dell'array allo stesso modo:

/* Assegniamo il valore del terzo elemento alla variabile y */
int y = x[2];

Per accedere ad un elemento di un array possiamo utilizzare una qualunque espressione che si traduce in un valore intero maggiore o uguale a zero.

Non possiamo, invece, utilizzare valori numerici di altro tipo come ad esempio un floating point.

/* Il seguente codice è valido */
int i = 2;
int j = 3;

x[i + j * 2] = 1;

/* Il seguente codice NON è valido */
float k = 1.0F;
x[k] = 0; /* ERRORE */

Limiti di un array

Una cosa fondamentale da tenere a mente quando si usano gli array in linguaggio C è che i limiti di un array non vengono controllati in automatico.

Nota

I limiti di un array non vengono controllati automaticamente

Lo standard del linguaggio C non impone che i limiti di un array vengano controllati nel momento in cui si accede ad un elemento dell'array stesso.

Nel caso in cui l'indice di un array sia fuori range il comportamento del programma diventa impredicibile.

Prendiamo lo stralcio di codice che segue:

/* Dichiariamo un array con 10 elementi */
int x[10];

/* Accediamo all'elemento undicesimo */
x[10] = 1;

In tal caso la riga x[10] = 1; accede all'elemento undicesimo che non esiste.

Nella pratica il programma accederà alla locazione di memoria che segue immediatamente il decimo elemento dell'array. Dato che questa locazione di memoria potrebbe appartenere ad un'altra variabile o struttura di controllo non possiamo sapere a priori cosa accadrà al nostro programma. Per tal motivo il comportamento del programma è impredicibile: potrebbe non succedere nulla oppure il nostro programma potrebbe andare in crash.

Nel caso in cui il programma vada in crash si parla di Segmentation Fault o Errore di Segmentazione.

Bisogna prestare molta attenzione quando si lavora con gli indici di un array in linguaggio C. Scrivere oltre i limiti di un array prende il nome di Sfondamento di un array o Array Overflow.

Alcuni linguaggi di programmazione come Java, C#, Python e Rust controllano l'accesso agli array e segnalano gli sfondamenti per rendere i programmi più robusti.

Gli ideatori del C decisero di non inserire meccanismi di controllo dello sfondamento di un array per rendere i programmi C più veloci. Infatti controllare i limiti di un array ha un costo sulle performance di un programma. Ovviamente questa scelta ha i suoi pro e i suoi contro.

Ricapitolando:

Definizione

Sfondamento di un array o Array Overflow

Lo Sfondamento di un array in linguaggio C si verifica quando si tenta di accedere ad un elemento di un array con un indice che supera il numero di elementi dell'array stesso.

In linguaggio C non viene effettuato nessun controllo sugli indici di un array per cui accedere ad una locazione oltre i limiti non è, di per sé, un'operazione illegale.

Tuttavia le conseguenze possono essere impredicibili. In molti casi il programma potrebbe andare in crash e l'evento correlato prende il nome di Segmentation Fault o Errore di Segmentazione.

Array e cicli

In linguaggio C gli array sono strettamente correlati ai cicli, in particolare ai cicli for.

La stragrande maggioranza dei programmi che ha a che fare con array ha al proprio interno stralci di codice per inizializzare, leggere, modificare e operare sugli elementi di un array. Proprio per il fatto che operazioni di questo tipo sono molto frequenti, in questa sezione riportiamo alcuni esempi idiomatici di utilizzo di cicli for per operare su array.

Inizializzare un array con un ciclo for

In linguaggio C così come per tutti i tipi di variabili anche gli array quando vengono dichiarati non sono mai inizializzati in maniera automatica. Motivo per cui, spesso, nasce l'esigenza di inizializzare gli elementi di un array ad un valore di partenza.

Un esempio tipico di inizializzazione degli elementi di un array a zero con un ciclo for è il seguente:

int x[10];
int i;

for (i = 0; i < 10; ++i) {
    x[i] = 0; /* Inizializza gli elementi a zero */
}

Esempio di caricamento dati in un array

Un altro esempio tipico di ciclo for che opera su un array è quello di caricare in input i valori di un array da tastiera attraverso scanf.

Nell'esempio che segue vengono letti 10 valori floating point da tastiera e memorizzati in un array:

double x[10];
int i;

for (i = 0; i < 10; ++i) {
    scanf("%f", &x[i]);
}

Operazioni cumulative su di un array

Spesso capita di dover effettuare delle operazioni cumulative su di un array, ossia operazioni che hanno bisogno di calcolare un valore a partire da tutti gli elementi di un array.

Un esempio è il calcolo della somma di tutti gli elementi di tipo double contenuti in un array. Per far questo basta combinare un ciclo for con l'utilizzo di una variabile di supporto che prende il nome di accumulatore:

double x[10];

/* Variabile accumulatrice. Inizializzata a zero */
double accumulator = 0.0;

int i;

/* Memorizzazione dei valori nell'array */
/* ... */

/* Operazione cumulativa di somma */
for (i = 0; i < 10; ++i) {
    accumulator += x[i];
}

printf("Somma totale: %f\n", accumulator);

Inizializzazione di un array

Un array, come qualunque altro tipo di variabile, può essere inizializzato al momento della dichiarazione.

Tuttavia, le regole per l'inizializzazione sono alquanto insidiose, motivo per cui in questa sezione le vedremo in dettaglio.

La forma più semplice di inizializzazione per array è una lista di valori separati da virgole e racchiusi tra parentesi graffe. Questa lista prende il nome di lista di inizializzazione di un array ed ha la sintassi seguente:

tipo_array nome_array[numero_elementi] = {elemento_1, elemento_2, ..., elemento_n};

Ad esempio, supponendo di avere un array di 5 numeri interi, un modo per inizializzarlo è il seguente:

int x[5] = {10, 20, 30, 40, 50};

Nel caso in cui la lista contenga meno elementi di quanti l'array ne contenga, gli elementi rimanenti dell'array vengono inizializzati a zero. Ad esempio:

int x[10] = {10, 20, 30, 40, 50};

In tal caso, l'array x è di 10 elementi ma nella lista di inizializzazione ci sono solo 5 elementi. Per cui l'array x conterrà al suo interno i valori:

{10, 20, 30, 40, 50, 0, 0, 0, 0, 0}

Gli ultimi cinque valori saranno pari a zero.

Un trucchetto molto utile è quello di sfruttare questa funzionalità per inizializzare tutti i valori dell'array a zero, in questo modo:

int x[10] = {0};

In questo modo la lista di inizializzazione è composta da un singolo elemento pari a zero. I restanti elementi saranno comunque inizializzati a zero. Per cui l'array x conterrà tutti i valori inizializzati a zero:

{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}

Abbiamo dovuto comunque inserire un singolo valore all'interno della lista di inizializzazione in quanto in linguaggio C è illegale avere una lista di inizializzazione vuota. Non avremmo potuto scrivere la riga di codice in questo modo:

/* ERRORE DI SINTASSI */
int x[10] = {};

Altra situazione non consentita in linguaggio C è quando la lista di inizializzazione ha più elementi dell'array. Ad esempio il seguente codice è illegale:

/* ERRORE: La lista di inizializzazione ha 5 elementi */
int x[4] = {10, 20, 30, 40, 50};

Nell'esempio l'array x è dichiarato di quattro elementi ma la lista di inizializzazione ne possiede cinque.

Per ovviare a questo problema, in linguaggio C è consentito omettere la lunghezza dell'array quando è presente una lista di inizializzazione. Ad esempio:

int x[] = {10, 20, 30, 40, 50};
/* L'array x conterrà 5 elementi */

In questo caso non abbiamo specificato tra le parentesi quadre il numero di elementi dell'array x ma il compilatore ha dedotto che l'array contenesse 5 elementi dalla lunghezza della lista di inizializzazione.

Ricapitolando:

Definizione

Lista di inizializzazione

Un array statico può essere inizializzato in C utilizzando una lista di inizializzazione.

La sintassi di una lista di inizializzazione è la seguente:

tipo nome[numero_elementi] = {elemento_1, elemento_2, ..., elemento_n};
  • Una lista di inizializzazione non può essere vuota;
  • Una lista di inizializzazione non può avere un numero di elementi maggiore del numero di elementi di un array;
  • Nel caso in cui una lista di inizializzazione ha meno elementi del numero di elementi di un array, gli elementi restanti sono inizializzati al valore di default del tipo;
  • Quando si usa una lista di inizializzazione, la dimensione dell'array può essere omessa. In tal caso la dimensione viene dedotta dalla dimensione della lista.

Inizializzatori designati in C99

Spesso, può accadere che soltanto pochi elementi di un array vadano inizializzati con un valore specifico mentre la maggioranza va inizializzata con un valore di default o zero.

Prendiamo ad esempio un array x di 20 elementi di cui soltanto 4 elementi vadano inizializzati ad un valore diverso da zero. Potremmo scrivere in questo modo:

int x[20] = {0, 0, 0, 0, 61, 0, 0, 0, 39, 0,
             0, 0, 0, 0, 0, 24, 0, 0, 56, 0};

In questo esempio abbiamo inizializzato il quinto elemento a 61, il nono elemento a 39, il sedicesimo elemento a 24 e il diciannovesimo elemento a 56. Tutti gli altri elementi li abbiamo inizializzati a zero.

Quando le dimensioni di un array diventano elevate, scrivere una lista di inizializzazione in questo modo diventa non solo tedioso ma soggetto ad errori. Si pensi, ad esempio, ad un array di 100 elementi di cui vogliamo inizializzarne soltanto una manciata.

Per risolvere questo problema, nello standard C99 sono stati introdotti gli Inizializzatori designati che possono essere usati nelle liste di inizializzazione. Ritornando all'esempio di prima possiamo riscrivere l'inizializzazione dell'array in questo modo:

int x[20] = {[5] = 61, [9] = 39, [16] = 24, [19] = 56};

Come si può osservare, nella lista di inizializzazione abbiamo specificato l'indice del valore tra parentesi quadre seguito dal segno di uguale e dal valore di inizializzazione. Tutti gli elementi non presenti nella lista sono automaticamente inizializzati a zero.

Il vantaggio di usare gli inizializzatori designati in C99 sta nel fatto che è più leggibile ed è meno soggetto ad errori. Inoltre, l'ordine di inizializzazione non conta più. Pertanto possiamo riscrivere la riga di sopra anche in questo modo:

int x[20] = {[9] = 39, [5] = 61, [19] = 56, [16] = 24};

Ovviamente, un inizializzatore designato deve essere un'espressione intera costante, ossia il cui valore sia noto a tempo di compilazione. Se l'array è di lunghezza n, tutti gli inizializzatori designati devono avere un valore compreso tra 0 e n - 1.

Gli inizializzatori designati possono essere anche usati nel caso in cui si omette la lunghezza dell'array. In tal caso la lunghezza dell'array viene dedotta dal designatore con l'indice più grande.

Ad esempio:

/* L'array avrà lunghezza 14 */
int x[] = {[1] = 7, [14] = 22, [5] = 9};

Nell'esempio di sopra abbiamo omesso la lunghezza dell'array. In tal caso il compilatore deduce che la lunghezza dell'array deve essere di 14 elementi in quanto è il più grande inizializzatore designato.

Infine, è possibile combinare il vecchio stile di inizializzazione con gli inizializzatori designati. Ad esempio:

int x[10] = {34, 23, [5] = 78, 54, [8] = 99};

In questo esempio l'array x avrà i seguenti valori:

{34, 23, 0, 0, 0, 78, 54, 0, 99, 0}

Tutti gli elementi non specificati nella lista saranno inizializzati a zero.

Ricapitolando:

Definizione

Inizializzatori designati in C99

Nello standard C99 è possibile inserire all'interno delle liste di inizializzazione degli inizializzatori designati.

La sintassi di un inizializzatore designato è la seguente:

[indice] = valore
  • Il valore dell'indice deve essere compreso tra 0 e la dimensione dell'array meno 1;
  • Usando gli inizializzatori designati l'ordine non conta;
  • Se la dimensione dell'array è omessa, viene usata come dimensione l'indice più alto tra gli inizializzatori;
  • Una lista di inizializzazione può contenere sia valori che inizializzatori designati. In tal caso i valori privi di inizializzatore designato vengono assegnati agli elementi con indice a partire dal successivo a quello del più vicino inizializzatore designato, oppure a partire da zero.

Array e operatore sizeof

L'operatore sizeof può essere usato per ottenere la dimensione di un array statico allo stesso modo di una variabile normale.

Ad esempio, supponendo che il tipo int occupi 64 bit, possiamo calcolare la dimensione di un array di 10 elementi interi in questo modo:

int x[10] = {0};
printf("Dimensione array: %d byte\n", sizeof(x));

Eseguendo questo stralcio di codice otteniamo il seguente output:

Dimensione array: 80 byte

Infatti, il risultato finale è di 80 byte, in quanto un intero int occupa 8 byte (se siamo su una macchina a 64 bit) quindi 8 \cdot 10 = 80 byte. Su una macchina a 32 bit il risultato potrebbe essere diverso.

L'operatore sizeof può essere anche adoperato per capire quanti elementi contiene un array statico. Ritornando all'esempio di prima possiamo scrivere in questo modo:

int numero_elementi;
numero_elementi = sizeof(x) / sizeof(x[0]);

Questa espressione funziona in quanto sizeof(x[0]) riporta la dimensione solo del primo elemento di un array. Motivo per cui stiamo dividendo la dimensione totale di un array per la dimensione di un suo singolo elemento.

Usando questa tecnica possiamo evitare di scrivere esplicitamente in un ciclo for la dimensione di un array. Ad esempio per effettuare la somma di tutti gli elementi di un array possiamo scrivere in questo modo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

int main() {
    int x[10] = {0};
    int i;
    int somma = 0;

    /* Legge da console i valori dell'array */
    for (i = 0; i < (sizeof(x) / sizeof(x[0])); ++i) {
        printf("Inserisci il valore %d: ", i + 1);
        scanf("%d", &x[i]);
    }

    /* Effettua la somma */
    for (i = 0; i < (sizeof(x) / sizeof(x[0])); ++i) {
        somma += x[i];
    }

    printf("Somma totale: %d\n", somma);

    return 0;
}

Come si può vedere dall'esempio, nelle righe 9 e 15 non abbiamo dovuto specificare direttamente la lunghezza del nostro array. In questo modo, se volessimo modificare il codice per gestire array con un numero di elementi diverso, basterebbe cambiare la dimensione dell'array alla riga 4.

Usando questa tecnica, l'unico problema che potrebbe sorgere è che molti compilatori potrebbero lamentarsi per l'espressione i < (sizeof(x) / sizeof(x[0])). Ad esempio, usando gcc e attivando le opzioni -Wextra -Wall che abilitano segnalazioni di warning extra, potremmo ottenere i seguenti messaggi di warning:

test.c: In function ‘main’:
test.c:9:19: warning: comparison of integer expressions of different signedness: ‘int’ and ‘long unsigned int’ [-Wsign-compare]
    9 |     for (i = 0; i < (sizeof(x) / sizeof(x[0])); ++i) {
      |                   ^
test.c:15:19: warning: comparison of integer expressions of different signedness: ‘int’ and ‘long unsigned int’ [-Wsign-compare]
   15 |     for (i = 0; i < (sizeof(x) / sizeof(x[0])); ++i) {
      |  

Con questi messaggi, il compilatore ci sta dicendo che stiamo comparando un int con segno, la variabile i, con il risultato di un'operazione senza segno. Infatti il risultato di un operatore sizeof è un valore size_t ossia un intero senza segno. Comparare un intero senza segno ed un intero con segno non è sempre una buona idea.

Possiamo evitare questo warning riscrivendo il ciclo for usando un cast esplicito in questo modo:

for (i = 0; i < (int) (sizeof(x) / sizeof(x[0])); ++i) {

Per cui il programma diventa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

int main() {
    int x[10] = {0};
    int i;
    int somma = 0;

    /* Legge da console i valori dell'array */
    for (i = 0; i < (int) (sizeof(x) / sizeof(x[0])); ++i) {
        printf("Inserisci il valore %d: ", i + 1);
        scanf("%d", &x[i]);
    }

    /* Effettua la somma */
    for (i = 0; i < (int) (sizeof(x) / sizeof(x[0])); ++i) {
        somma += x[i];
    }

    printf("Somma totale: %d\n", somma);

    return 0;
}

A questo punto il compilatore non si lamenterà più in quanto abbiamo esplicitato il fatto che stiamo comparando un intero signed con uno unsigned.

In Sintesi

In questa lezione abbiamo visto come creare e utilizzare gli array statici monodimensionali in C. Abbiamo discusso come dichiarare un array, come assegnare valori agli elementi e come accedere agli stessi utilizzando l'operatore di indice []. Abbiamo anche fornito esempi pratici per mostrare come questi concetti possono essere utilizzati nella programmazione reale.

Gli array statici monodimensionali sono una delle strutture dati più importanti e versatili in C e sono utilizzati in molti tipi di programmi. E' importante saperli utilizzare in modo efficace per sfruttare al meglio le loro potenzialità.

Tuttavia, gli array statici monodimensionali hanno alcune limitazioni, tra cui la dimensione fissa e l'impossibilità di modificarla una volta dichiarata. In future lezioni discuteremo di altre tipi di array e di come superare queste limitazioni.

Nella prossima lezione approfondiremo lo studio degli array statici multidimensionali i cui elementi sono indicizzabili con più indici.