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); }