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:
- Alla fine del primo ciclo la variabile
sum
, che inizialmente vale 0, viene incrementata dia[0]
. La situazione è riportata in figura:
- Alla fine del secondo ciclo il puntatore
p
punta all'elemento successivo, ossiaa[1]
. Inoltre, la variabilesum
viene incrementata dia[1]
. La situazione è riportata in figura:
- Alla fine del terzo ciclo il puntatore
p
punta all'elemento successivo, ossiaa[2]
. Inoltre, la variabilesum
viene incrementata dia[2]
. La situazione è riportata in figura:
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 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:
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.
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:
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.
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;
}
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.
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 funzionetrova_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.
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.
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.
Puntatori ad Array a Lunghezza Variabile in C99
Lo standard C99 ha introdotto gli array a lunghezza variabile che consentono di dichiarare array con una lunghezza determinata a tempo di esecuzione.
Sempre in C99 è possibile utilizzare dei puntatori per puntare ad essi allo stesso modo di un array qualunque.
Ad esempio:
int n = 10;
int a[n];
int *p = a;
In questo caso p
è un puntatore che punta all'array a
di lunghezza n
. Su p
possiamo utilizzare l'aritmetica dei puntatori e accedere agli elementi dell'array allo stesso modo di un array qualunque:
for (int i = 0; i < n; i++) {
p[i] = i;
}
Puntatori ad Array a Lunghezza Variabile in C99
In C99 è possibile utilizzare dei puntatori per puntare ad array a lunghezza variabile allo stesso modo di un array qualunque.
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.