Vettori, indirizzi e puntatori
Finora abbiamo usato gli indirizzi nel chiamare
 la funzione scanf()
 le altre funzioni per riferimento
Tuttavia la vera potenza dei puntatori è nelle operazioni con vettori,
stringhe e altre strutture dati, poiché esiste una relazione diretta e
stretta tra vettori, indirizzi e puntatori. Mostriamo intanto che:
il riferimento agli elementi di un vettore
può avvenire tramite i puntatori
Puntatori come nomi di vettori. Riprendiamo in considerazione il
precedente vettore voti, costituito da 5 interi.
Osserviamo che l’uso di un subscritto nasconde l’uso che il
computer fa degli indirizzi. Di fatto, il computer usa il subscritto per
calcolare l’indirizzo dell’elemento desiderato basandosi su:
 l’indirizzo di partenza del vettore
 la quantità di memoria usata per ciascun elemento.
Quindi la chiamata, ad es., del 4° elemento, voti[3], fa eseguire
al computer il seguente calcolo:
&voti[3] = &voti[0] + (3 * 2)
come è illustrato dalla figura seguente:
Perciò, se creiamo un puntatore che memorizzi l’indirizzo del 1°
elemento del vettore voti, possiamo accedere agli elementi del
vettore simulando la modalià seguita dal computer.
A tale fine potremmo:
• memorizzare l’indirizzo dell’elemento voti[0] nel puntatore
punt_v, con l’istruzione di assegnazione
punt_v = &voti[0];
• usare l’operatore di indirezione * e l’indirizzo memorizzato nel
puntatore per accedere a ciascun elemento del vettore.
In tal modo l’espressione
*punt_v
(“la variabile puntata da punt_v”) è un riferimento a
voti[0], come mostra la figura seguente:
Notazione con puntatore e offset. Una caratteristica unica dei
puntatori è che nelle espressioni che li usano si possono inserire gli
offset. Ad es., nell’espressione
*(punt_v + 1)
1 è un offset, che rende l’espressione un riferimento alla variabile
che segue di 1 posizione la variabile puntata da punt_v, ossia
voti[1].
Analogamente, l’espressione *(punt_v + 3) è un riferimento alla
variabile che segue di 3 posizioni la variabile puntata da punt_v,
ossia voti[3], come illustra la seguente tabella
Usando la corrispondenza tra puntatori e subscritti, abbiamo adesso
due possibilità diverse per accedere agli elementi del vettore voti:
1. con un programma che usi i subscritti, quale:
#include <stdio.h>
void main(void)
{
int i;
int voti[] = {98, 87, 92, 79, 85};
for (i = 0; i <= 4; ++i)
printf("\nL’elemento %d è %d", i, voti[i]);
}
2. con un programma che usi i puntatori, quale:
#include <stdio.h>
void main(void)
{
int *punt_v;
int i;
int voti[] = {98, 87, 92, 79, 85};
punt_v = &voti[0];
for (i = 0; i <= 4; ++i)
printf("\nL’elemento %d è %d", i, *(punt_v + i));
}
Essi producono, ovviamente, la stessa uscita:
Il metodo usato nel programma precedente per accedere ai singoli
elementi del vettore simula quello con cui un computer fa
riferimento agli elementi del vettore.
Ogni subscritto usato dal programmatore viene convertito
automaticamente in un’equivalente espressione con puntatori.
Nel nostro caso, dato che la dichiarazione di punt_v comprende
l’informazione che le variabili puntate sono interi, ogni offset
aggiunto all’indirizzo in punt_v viene automaticamente scalato
secondo la dimensione di un intero.
Così, ad es., *(punt_v + 3) è un riferimento all’indirizzo di
voti[0] più un offset di 6 byte (3 * 2), come illustrato in una
figura precedente.
Osservazioni
1. Le parentesi nell’espressione
*(punt_v + 3)
sono necessarie per un riferimento corretto all’elemento desiderato.
Infatti la loro omissione darebbe luogo all’espressione
*punt_v + 3
che aggiunge 3 alla “variabile puntata da punt_v”, che è voti[0].
2. L’espressione
*(punt_v + 3)
non cambia l’indirizzo memorizzato in punt_v.
Una volta che il computer abbia usato l’offset per localizzare la
variabile corretta a partire dall’indirizzo di partenza in punt_v,
l’offset è eliminato e l’indirizzo in punt_v rimane inalterato.
Costanti puntatore. Sebbene il puntatore punt_v usato nel
programma precedente sia stato creato per memorizzare l’indirizzo
di partenza del vettore voti, ciò non era in realtà necessario.
Infatti, quando si dichiara un vettore, il compilatore crea
automaticamente per esso una costante puntatore interna, nella
quale memorizza l’indirizzo di partenza del vettore.
Una costante puntatore è identica a una variabile puntatore creata
dal programmatore sotto molti aspetti (ma, come vedremo, non
tutti).
Per ogni vettore creato, il suo nome diventa quello della costante
puntatore che il compilatore ha creato per esso, e nella quale viene
memorizzato l’indirizzo di partenza della prima locazione riservata
per il vettore.
Così, le dichiarazioni del vettore voti nei due programmi precedenti
hanno effettivamente:
 riservato memoria sufficiente per 5 interi
 creato una costante puntatore interna di nome voti, nella quale
hanno memorizzato l’indirizo di voti[0].
Perciò, il programma precedente si può anche semplificare
sopprimendo le due istruzioni scritte in chiaro:
#include <stdio.h>
void main(void)
{
int *punt_v;
int i;
int voti[] = {98, 87, 92, 79, 85};
punt_v = &voti[0];
for (i = 0; i <= 4; ++i)
printf("\nL’elemento %d è %d", i, *(voti + i));
}
In esso, avendo usato voti come puntatore, abbiamo eliminato le
istruzioni int *punt_v; (che dichiarava il puntatore punt_v) e
punt_v = &voti[0]; (che lo inizializzava).
Sotto molti aspetti,
un nome di vettore e un puntatore si
possono usare in modo intercambiabile.
Tuttavia,
un vero puntatore è una variabile, e l’indirizzo
memorizzato in esso può essere cambiato
mentre
un nome di vettore è una costante puntatore, e l’indrizzo memorizzato
in essa non può essere cambiato da un’istruzione di assegnazione.
Perciò non sarebbe valida un’espressione del tipo
voti = &voti[2]
Infatti un nome di vettore ha lo scopo di localizzare correttamente
l’inizio del vettore, e tale scopo verebbe meno se fosse consentito
alterare l’indirizzo memorizzato nel nome del vettore.
Inoltre,
non sarebbe valida un’espressione contenente
l’indirizzo di un nome di vettore, quale &voti
Ciò perché il puntatore creato dal compilatore è interno a esso e non
memorizzato in memoria, come le variabili puntatore.
Un interessante complemento al fatto che il riferimento agli elementi
di un vettore può avvenire tramite i puntatori è che anche
un riferimento puntatore si può sempre
sostituire con un riferimento subscritto
Ad es., se punt_num è una variabile puntatore, l’espressione
*(punt_num + i)
può essere sostituita da
punt_num[i]
anche se punt_num non è creato come vettore. Come prima,
quando il compilatore incontra la notazione con subscritto la
sostituisce internamente con quella a puntatore.
Aritmetica dei puntatori
Le variabili puntatore, come tutte le altre, contengono valori, che nel
caso specifico sono indirizzi.
Perciò, sommando e sottraendo numeri ai puntatori possiamo ottenere
indirizzi differenti.
Inoltre, gli indirizzi nei puntatori possono essere confrontati usando
qualsiasi operatore relazionale (==, <=, ...) valido per confrontare le
altre variabili.
Nell’eseguire i calcoli con i puntatori si deve porre attenzione a
produrre indirizzi che puntino a qualcosa di significativo, mentre nel
confrontare i puntatori si devono effettuare confronti che abbiano
senso.
Consideriamo le due dichiarazioni:
int num[100];
int *punt_n;
per assegnare a punt_n l’indirizzo di num[0] si può usare, oltre
alla ovvia istruzione di assegnazione:
punt_n = &num[0];
anche la meno ovvia istruzione:
punt_n = num;
Esse producono lo stesso risultato perché num è una costante
puntatore che contiene l’indirizzo della prima componente del
vettore, ossia l’indirizzo di num[0].
La figura seguente illustra l’allocazione di memoria risultante dalle
precedenti istruzioni di dichiarazione e assegnazione, nell’ipotesi
che la locazione d’inizio del vettore num sia l’indirizzo 18934
Una volta che punt_n contenga un indirizzo valido, gli si possono
aggiungere e sottrarre valori per produrre nuovi indirizzi.
Quando si aggiungono o sottraggono numeri ai puntatori, il computer
li aggiusta automaticamente per garantire che il risultato punti
ancora a un valore del tipo corretto.
Ad es., l’istruzione:
punt_n = punt_n + 4;
forza il computer a scalare il 4 del numero corretto per garantire che
l’indirizzo risultante sia quello di un intero.
Dato che ogni intero richiede 2 byte di memoria, il computer
moltiplica il 4 per 2 e quindi aggiunge 8 all’indirizzo in punt_n,
ottenendo l’indirizzo 18942, che è quello corretto di num[4].
Gli indirizzi possono essere incrementati o decrementati usando
anche gli operatori di incremento prefissi e postfissi: l’aggiunta di 1
a un puntatore lo fa puntare all’elemento successivo, la
sottrazione di 1 lo fa puntare al precedente.
Sono quindi valide le seguenti combinazioni:
*punt_num++
*++punt_num
*punt_num-*--punt_num
/*
/*
/*
/*
usa il puntatore e poi lo
incrementa il puntatore e
usa il puntatore e poi lo
decrementa il puntatore e
incrementa
poi lo usa
decremente
poi lo usa
*/
*/
*/
*/
Di esse la più usata è *punt_num++, che consente di accedere a
ogni elemento di un vettore via via che si procede in esso dal suo
indirizzo di partenza a quello dell’ultimo elemento.
Un esempio del suo utilizzo è fornito nel programma seguente, che
calcola la somma degli elementi di un vettore.
#include <stdio.h>
void main(void)
{
int numeri[5] = {16, 54, 7, 43, -5};
int i, somma = 0, *punt_num;
punt_num = numeri; /*memorizza l’indirizzo
di numeri[0] in punt_num*/
for (i = 0; i <= 4; ++i)
somma = somma + *punt_num++;
printf(“La somma degli elementi del vettore è %d",somma);
}
I puntatori possono anche essere confrontati, il che risulta utile
quando si usano puntatori che puntano a elementi del medesimo
vettore.
Ad es., per accedere ai vari elementi del vettore, anziché usare un
contatore in un ciclo for si può confrontare l’indirizzo in un
puntatore con gli indirizzi iniziale e finale del vettore stesso.
L’espressione
punt_num <= &numeri[4]
è vera (diversa da zero) finché l’indrizzo in punt_num è minore o
uguale all’indirizzo di numeri[4].
Dato che numeri è una costante puntatore che contiene l’indirizzo
di numeri[0], il termine &numeri[4] può essere sostituito dal
termine equivalente numeri + 4.
Usando una di queste due forme, il programma precedente si può
riscrivere come il seguente, che continua ad aggiungere gli
elementi del vettore finché l’indirizzo in punt_num è minore o
uguale all’indrizzo dell’ultimo elemento del vettore.
#include <stdio.h>
void main(void)
{
int numeri[5] = {16, 54, 7, 43, -5};
int totale = 0, *punt_num;
punt_num = numeri;
/* memorizza l'indirizzo di
numeri[0] in punt_num */
while (punt_num <= numeri + 4)
somma += *punt_num++;
printf(“La somma degli elementi del vettore è:
%d",somma);
}
Scarica

Fonda19