Puntatori e Array in linguaggio C

Puntatori e Array in linguaggio C sono strettamente legati. Utilizzando, infatti, l'aritmetica dei puntatori è possibile manipolare gli elementi di un array.

Analogamente, in C, un array è un puntatore costante che punta all'elemento iniziale dell'array. Questa lezione spiega nel dettaglio come funzionano puntatori e array in linguaggio C.

Processamento degli array con i puntatori

L'aritmetica dei puntatori ci permette di manipolare gli elementi di un array incrementando o decrementando ripetutamente una variabile puntatore.

Ad esempio, supponiamo di voler sommare tutti gli elementi contenuti in un array di n elementi. Possiamo definire una variabile somma per memorizzare la somma e una variabile puntatore p per memorizzare l'indirizzo dell'elemento corrente.

A questo scopo possiamo usare un ciclo for. Inizializziamo la variabile puntatore all'indirizzo del primo elemento dell'array e incrementiamo il puntatore ad ogni iterazione del ciclo. Quando il puntatore raggiunge l'indirizzo successivo all'ultimo elemento dell'array, il ciclo termina.

const int n = 10;
int a[n];
int sum = 0;
int *p;

/* Inizializzazione dell'array */
/* ... */

for (p = &a[0]; p < &a[n]; p++) {
    sum += *p;
}

Per comprendere meglio, guardiamo cosa accade ad ogni iterazione del ciclo for.

  • All'inizio del ciclo p viene inizializzato puntando al primo elemento dell'array. La situazione è riportata in figura:
Somma degli elementi di un array: Situazione Iniziale
Figura 1: Somma degli elementi di un array: Situazione Iniziale
  • Alla fine del primo ciclo la variabile sum, che inizialmente vale 0, viene incrementata di a[0]. La situazione è riportata in figura:
Somma degli elementi di un array: Fine prima iterazione
Figura 2: Somma degli elementi di un array: Fine prima iterazione
  • Alla fine del secondo ciclo il puntatore p punta all'elemento successivo, ossia a[1]. Inoltre, la variabile sum viene incrementata di a[1]. La situazione è riportata in figura:
Somma degli elementi di un array: Fine seconda iterazione
Figura 3: Somma degli elementi di un array: Fine seconda iterazione
  • Alla fine del terzo ciclo il puntatore p punta all'elemento successivo, ossia a[2]. Inoltre, la variabile sum viene incrementata di a[2]. La situazione è riportata in figura:
Somma degli elementi di un array: Fine terza iterazione
Figura 4: Somma degli elementi di un array: Fine terza iterazione

La condizione di terminazione del ciclo for è p < &a[n]. Questa condizione è vera finché il puntatore p non raggiunge l'indirizzo dell'elemento successivo all'ultimo elemento dell'array. In altre parole, il ciclo termina quando il puntatore p raggiunge l'indirizzo dell'elemento a[n].

A prima vista potrebbe sembrare illegale prendere l'indirizzo di a[n], ossia di un elemento che non esiste in quanto l'array a va da 0 a n-1. In realtà, far questo è sicuro in quanto il programma non accede alla locazione a[n]. Esso controlla soltanto che l'indirizzo di p abbia raggiunto l'indirizzo di a[n]. In altre parole, nel programma non si tenta di effettuare l'operazione illegale di dereferenziazione del puntatore p quando questo punta ad a[n].

La situazione finale è la seguente:

Somma degli elementi di un array: Situazione Finale
Figura 5: Somma degli elementi di un array: Situazione Finale

Questo esempio mostra come l'aritmetica dei puntatori può essere usata per manipolare gli elementi di un array. Avremmo potuto, in alternativa, usare una variabile intera i per indicizzare gli elementi dell'array.

Nome di un array come puntatore

L'aritmetica dei puntatori è uno dei modi con cui array e puntatori sono collegati. Esiste tuttavia un'altra relazione.

Definizione

Nome di un array come puntatore

Nel linguaggio C il nome di un array rappresenta l'indirizzo del primo elemento dell'array. Questo significa che il nome di un array può essere usato come puntatore.

Questa relazione semplifica l'aritmetica dei puntatori e rende array e puntatori molto più versatili.

Per comprendere nel dettaglio, supponiamo di avere un array composto da 10 interi:

int a[10];

In base a quanto detto prima, possiamo usare l'identificatore a come un puntatore che punta all'elemento a[0]. Possiamo quindi modificare l'elemento a[0] in questo modo:

/* Memorizza il valore 10 nell'elemento a[0] */
*a = 10;

Possiamo anche usare l'aritmetica dei puntatori per accedere agli altri elementi dell'array. Ad esempio possiamo modificare il secondo elemento, a[1], in questo modo:

/* Memorizza il valore 20 nell'elemento a[1] */
*(a + 1) = 20;

In generale, quindi, possiamo affermare che:

Definizione

Array come puntatori: Espressioni equivalenti

Dal momento che il nome di un array equivale ad un puntatore al primo elemento, le seguenti espressioni sono equivalenti tra di loro:

  • L'indirizzo dell'elemento i-esimo di un array equivale all'indirizzo del primo elemento dell'array incrementato di i:

    &a[i] == a + i
    
  • Accedere all'elemento i-esimo di un array equivale a dereferenziare il puntatore al primo elemento dell'array incrementato di i:

    *(a + i) == a[i]
    

In altre parole, l'indicizzazione di un array è una forma alternativa di aritmetica dei puntatori.

Consiglio

Il fatto che il nome di un array equivale ad un puntatore al primo elemento dell'array ci permette di semplificare anche i cicli for che scorrono gli array. Ad esempio, tornando al caso della somma degli elementi di un array, abbiamo il seguente ciclo for:

for (p = &a[0]; p < &a[n]; p++) {
    sum += *p;
}

Questo for può essere riscritto in maniera molto più semplice in questo modo:

for (p = a; p < a + n; p++) {
    sum += *p;
}
Nota

Il nome di un array è un puntatore costante

Sebbene il nome di un array può essere utilizzato come puntatore, non è possibile assegnare ad esso un nuovo indirizzo. Questo perché il nome di un array è una costante e non può essere modificato.

Ad esempio, un'espressione del genere provoca un errore del compilatore:

/* ERRORE */
a = &a[1];

Tuttavia è sempre possibile utilizzare un puntatore di supporto a cui assegnare l'indirizzo dell'array e modificarlo successivamente:

int *p = a;
p = &a[1];

Array come argomento di una funzione

Quando passiamo ad una funzione un array come argomento, il parametro della funzione sarà sempre trattato come un puntatore. Questo significa che la funzione non riceve l'array, ma riceve un puntatore al primo elemento dell'array.

Ad esempio, esaminiamo la funzione seguente che cerca e restituisce il massimo elemento di un array di interi:

int trova_massimo(int a[], int n) {
    int i;
    int max = a[0];
    for (i = 1; i < n; i++) {
        if (a[i] > max) {
            max = a[i];
        }
    }
    return max;
}

Adesso, supponiamo di invocare la funzione in questo modo:

int a[10];
/* ... */
int max = trova_massimo(a, 10);

Quello che accade è che la funzione riceve un puntatore al primo elemento dell'array a. In altre parole, la funzione riceve un puntatore a a[0]. La conseguenza importante di ciò è che l'array non viene copiato nella funzione.

Definizione

Array come parametri di una funzione

Un array passato come argomento di una funzione viene sempre passato per riferimento. Quindi, quando un array viene passato ad una funzione, la funzione riceve un puntatore al primo elemento dell'array.

Come conseguenza le due seguenti dichiarazioni sono equivalenti:

int trova_massimo(int a[], int n);

int trova_massimo(int *a, int n);

Questo ha delle importanti conseguenze:

  • Quando una variabile ordinaria viene passata ad una funzione, il suo valore viene copiato. Ogni cambiamento applicato al parametro non ha alcun effetto sulla variabile originale. Viceversa, quando un array viene utilizzato come argomento di una funzione esso viene passato per riferimento e quindi non è protetto dalle eventuali modifiche che la funzione effettua. Ad esempio, possiamo scrivere una funzione inizializza che prende in ingresso un array e inizializza ogni singolo elemento a zero:

    void inizializza(int a[], int n) {
        int i;
        for (i = 0; i < n; i++) {
            a[i] = 0;
        }
    }
    

    Quando si vuole indicare che la funzione non modifica il contenuto di un array, è possibile aggiungere la parola chiave const al parametro dell'array. Ad esempio, la funzione trova_massimo non modifica il contenuto dell'array, quindi possiamo scrivere:

    int trova_massimo(const int a[], int n) {
        /* ... */
    }
    

    Questo ci permette di invocare la funzione in questo modo:

    int a[10];
    /* ... */
    int max = trova_massimo(a, 10);
    

    In questo modo, il compilatore ci avvisa se la funzione tenta di modificare il contenuto dell'array.

  • Dato che un array non viene passato per copia ad una funzione, il tempo impiegato per passarlo è indipendente dalla sua dimensione. Quindi il passaggio di un array è efficiente dal punto di vista delle performance.

  • Anziché dichiarare un parametro come array, possiamo sempre dichiararlo come puntatore. Ad esempio, tornando al caso della funzione trova_massimo, possiamo riscrivere la funzione in questo modo:

    int trova_massimo(int *a, int n) {
        /* ... */
    }
    

    Il compilatore tratta le due dichiarazioni come identiche.

Slicing

Dal momento che un array passato ad una funzione è trattato come un puntatore, è possibile utilizzare una tecnica particolare per lavorare soltanto su una porzione di un array. Questa tecnica è detta slicing o partizionamento.

Per comprendere come funziona, torniamo all'esempio della funzione trova_massimo. Supponiamo di voler trovare il massimo elemento di un sottoinsieme degli elementi di un array. Per fare questo non è necessario modificare la funzione, basta soltanto invocarla in maniera differente.

Ad esempio, se a è un array composto da 10 elementi ma vogliamo trovare il massimo soltanto tra gli elementi di indice 2, 3 e 4, possiamo invocare la funzione in questo modo:

int max = trova_massimo(&a[2], 3);

In questo modo, la funzione riceve un puntatore a a[2] e il numero di elementi da considerare è 3. La funzione lavorerà pertanto soltanto sugli elementi a[2], a[3] e a[4].

Allo stesso modo possiamo trovare il massimo tra gli ultimi 5 elementi in questo modo:

int max = trova_massimo(&a[5], 5);

La tecnica dello slicing funziona soltanto se la partizione comprende elementi consecutivi. Non è possibile partizionare l'array mettendo insieme elementi non contigui.

Definizione

Slicing o Partizionamento di un array

Lo slicing o partizionamento di un array consiste nell'invocare una funzione passando come argomento un puntatore all'elemento di indice i di un array e il numero di elementi da considerare.

Lo slicing può essere applicato soltanto a sottoinsiemi di elementi consecutivi di un array.

Nota

Attenzione: quando si utilizza la tecnica dello slicing è necessario assicurarsi che la dimensione della partizione non superi la dimensione dell'array. Se questo accade, si rischia di accedere a zone di memoria non assegnate all'array.

Usare un puntatore come un array

Dal momento che il nome di un array è un puntatore, risulta vero anche il contrario, ossia che un puntatore può essere usato come un array.

Infatti, è possibile usare l'operatore di indicizzazione [] per accedere a locazioni di memoria contigue a quelle puntate dal puntatore stesso.

Ad esempio, supponiamo di avere un puntatore p che punta ad un array di interi. Possiamo accedere agli elementi dell'array in questo modo:

int a[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int *p = a;

printf("%d\n", p[0]); // stampa 0
printf("%d\n", p[1]); // stampa 1
printf("%d\n", p[2]); // stampa 2

A tutti gli effetti, il compilatore tratta un'espressione del tipo p[i] come se fosse *(p + i). Questa funzionalità ci tornerà utile quando utilizzeremo gli array dinamici nelle prossime lezioni.

In Sintesi

In questa lezione abbiamo approfondito nel dettaglio la relazione che esiste tra gli array e i puntatori. In particolare abbiamo visto:

  • Come accedere agli elementi di un array sfruttando l'aritmetica dei puntatori;
  • Il nome di un array rappresenta a tutti gli effetti un puntatore al primo elemento dell'array;
  • Quando un array viene passato ad una funzione, esso viene passato per riferimento ossia come puntatore;
  • È possibile utilizzare la tecnica dello slicing per lavorare su una porzione di un array;
  • Un puntatore può essere usato come un array.

Nella prossima lezione vedremo la relazione che intercorre tra puntatori e array multidimensionali.