Aritmetica dei Puntatori in linguaggio C

L'aritmetica dei puntatori rappresenta uno dei concetti fondamentali della programmazione in linguaggio C. Attraverso di essa è possibile sfruttare al meglio la potenza dei puntatori.

In linguaggio C sono ammesse solo tre operazioni aritmetiche su di un puntatore:

  • Addizione di un intero ad un puntatore;
  • Sottrazione di un intero ad un puntatore;
  • Sottrazione di un puntatore da un altro puntatore.

In questa lezione vedremo come funzionano queste operazioni e vedremo anche come sfruttarle per accedere agli elementi di un array.

Aritmetica dei Puntatori

Nelle precedenti lezioni abbiamo visto due operazioni che è possibile effettuare su di un puntatore:

  • L'assegnamento di un indirizzo ad un puntatore attraverso l'operatore indirizzo &:

    int a = 10;
    int *p = &a;
    
  • L'accesso al valore puntato attraverso l'operatore di indirizzamento *:

    int a = 10;
    int *p = &a;
    int b = *p;
    

Con queste due operazioni di base siamo riusciti a realizzare degli alias per le variabili e siamo riusciti a passare alle funzioni degli argomenti per riferimento.

In questa lezione vedremo altri tipi di operazioni che è possibile effettuare su di un puntatore e soprattutto il legame stretto che esiste tra i puntatori e gli array.

In precedenza abbiamo accennato al fatto che un puntatore possa far riferimento agli elementi di un array. Ad esempio, supponiamo di avere un array di 10 interi:

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

Possiamo definire un puntatore che faccia riferimento all'elemento con indice 5 dell'array ad esempio in questo modo:

int *p = &a[5];

Quello che accade è riportato in figura:

Puntatore che punta all'elemento con indice 5 dell'array
Figura 1: Puntatore che punta all'elemento con indice 5 dell'array

Da questo momento in poi, utilizzando p possiamo modificare il valore dell'elemento con indice 5 dell'array a:

*p = 100;

In questo modo, l'array a avrà il seguente contenuto:

int a[10] = { 1, 2, 3, 4, 5, 100, 7, 8, 9, 10 };

La situazione è riportata in figura:

Puntatore che punta all'elemento con indice 5 dell'array
Figura 2: Puntatore che punta all'elemento con indice 5 dell'array

Modificare il valore di un singolo elemento di un array non ha una grande utilità in se. Tuttavia, attraverso l'aritmetica dei puntatori è possibile effettuare operazioni molto più complesse. Attraverso di essa è possibile accedere agli altri elementi di un array.

Prima di vedere come dobbiamo studiare quali operazioni aritmetiche sono consentite su di un puntatore:

  • Addizione di un intero ad un puntatore;
  • Sottrazione di un intero ad un puntatore;
  • Sottrazione di un puntatore da un altro puntatore.

Su di un puntatore sono ammesse solo queste tre operazioni in linguaggio C. Ora le vedremo nel dettaglio.

Addizione di un intero ad un puntatore

Supponiamo di avere un array di 10 interi e un puntatore p ad intero:

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

Inizializziamo il puntatore p facendolo puntare all'elemento con indice 3 dell'array a:

p = &a[3];

Quindi la situazione iniziale sarà la seguente:

Somma di un Puntatore e di un intero - Situazione Iniziale
Figura 3: Somma di un Puntatore e di un intero - Situazione Iniziale

Se sommiamo un intero n al puntatore p il risultato sarà un puntatore che fa riferimento ad n elementi dopo. In particolare, se p punta alla locazione a[3] e sommiamo n al puntatore p, il risultato sarà un puntatore che punta alla locazione a[3 + n]. Vediamo con un esempio:

p = &a[3];
p = p + 2;

Adesso p punta alla locazione a[3 + 2] ovvero alla locazione a[5]. In questo modo, possiamo accedere al valore dell'elemento con indice 5 dell'array a.

In parole povere, il puntatore si è spostato di due locazioni in avanti. La situazione finale è riportata in figura:

Somma di un Puntatore e di un intero - Situazione Finale
Figura 4: Somma di un Puntatore e di un intero - Situazione Finale

Questo però non vuol dire che all'indirizzo che contiene il puntatore sia stato sommato 2. Per capire meglio cosa succede, vediamo un esempio:

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

int *p = &a[3];

/* Stampiamo a schermo l'indirizzo contenuto in p */
printf("Indirizzo di p: %p\n", p);

/* Sommiamo 2 al puntatore p */
p = p + 2;

/* Stampiamo a schermo l'indirizzo contenuto in p */
printf("Indirizzo di p: %p\n", p);

Se proviamo ad eseguire questo stralcio di codice su di una macchina a 64 bit quello che possiamo ottenere è il seguente output:

Indirizzo di p: 0x7ffc0d1f678c
Indirizzo di p: 0x7ffc0d1f6794

In pratica prima dell'operazione di somma il puntatore p puntava all'indirizzo 0x7ffc0d1f678c che vale 140720528648076. Successivamente il puntatore p punta all'indirizzo 0x7ffc0d1f6794 che vale 140720528648084. In altre parole, il puntatore p si è spostato di 8 byte e non di due byte come si potrebbe pensare. Perché?

La motivazione sta nel fatto che quando si somma un intero ad un puntatore, il risultato è un puntatore che punta ad un indirizzo pari all'indirizzo di partenza più il numero di byte che occupa il tipo puntato moltiplicato per l'intero sommato.

Poiché p è un puntatore ad un intero e un intero in questo caso occupa 4 byte, il risultato sarà un puntatore che punta ad un indirizzo pari all'indirizzo di partenza più 4 moltiplicato per 2.

Possiamo confermare il tutto con questo esempio:

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

int *p = &a[3];

/* Otteniamo l'indirizzo di p */
unsigned long int indirizzo_p = (long int) p;

/* Stampiamo a schermo l'indirizzo contenuto in p */
printf("Indirizzo di p:                   %p\n", p);

/* Stampiamo la dimensione di un intero */
printf("Dimensione di un intero:          %d\n", sizeof(int));

/* Sommiamo 2 al puntatore p */
p = p + 2;

/* Stampiamo a schermo l'indirizzo contenuto in p */
printf("Indirizzo di p:                   %p\n", p);

/*
* Stampiamo il risultato della somma dell'indirizzo di p
* con la dimensione di un intero moltiplicata per 2
*/
printf("Indirizzo di p + 2 * sizeof(int): %lx\n",
        (indirizzo_p + 2 * sizeof(int)));

Se proviamo ad eseguire questo codice otteniamo:

Indirizzo di p:                   0x7fffedb4e35c
Dimensione di un intero:          4
Indirizzo di p:                   0x7fffedb4e364
Indirizzo di p + 2 * sizeof(int): 0x7fffedb4e364

Questo ci conferma quello che abbiamo ottenuto in precedenza. La dimensione di un intero è 4 byte e il risultato della somma dell'indirizzo di p con la dimensione di un intero moltiplicata per 2 è uguale all'indirizzo di p dopo l'operazione di somma.

Definizione

Somma di un intero ad un puntatore

Sommare un intero ad un puntatore significa ottenere un puntatore che punta ad un indirizzo pari all'indirizzo di partenza più il numero di byte che occupa il tipo puntato moltiplicato per l'intero sommato.

In termini formali se p è un puntatore ad un tipo tipo:

tipo *p;

tipo è di d byte:

d = sizeof(tipo);

e n è un intero, allora:

p = p + n;

è equivalente a:

\mbox{indirizzo}(p) = \mbox{indirizzo}(p) + n \cdot d

Sottrazione di un intero ad un puntatore

Analogamente al caso della somma, possiamo sottrarre un intero ad un puntatore. In questo caso, il risultato sarà un puntatore che punta ad un indirizzo pari all'indirizzo di partenza meno il numero di byte che occupa il tipo puntato moltiplicato per l'intero sottratto.

Ritornando al caso degli array, se abbiamo un array a di 10 elementi:

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

e un puntatore p che punta all'elemento di indice 3:

int *p = &a[3];

La situazione iniziale è la seguente:

Differenza tra un Puntatore e un intero - Situazione Iniziale
Figura 5: Differenza tra un Puntatore e un intero - Situazione Iniziale

possiamo sottrarre 2 all'elemento puntato da p:

p = p - 2;

In questo modo, p punterà all'elemento di indice 1. La situazione finale sarà la seguente:

Differenza tra un Puntatore e un intero - Situazione Finale
Figura 6: Differenza tra un Puntatore e un intero - Situazione Finale

Possiamo verificare il tutto con questo esempio:

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

int *p = &a[3];

/* Stampiamo a schermo l'indirizzo contenuto in p */
printf("Indirizzo di p: %p\n", p);

/* Sottraiamo 2 al puntatore p */
p = p - 2;

/* Stampiamo a schermo l'indirizzo contenuto in p */
printf("Indirizzo di p: %p\n", p);

Se proviamo ad eseguire questo stralcio di codice su di una macchina a 64 bit quello che possiamo ottenere è il seguente output:

Indirizzo di p: 0x7ffc0d1f678c
Indirizzo di p: 0x7ffc0d1f6774

Come si può osservare, dopo aver sottratto l'intero 2 al puntatore p questo si è spostato di 8 byte all'indietro.

Definizione

Sottrazione di un intero ad un puntatore

Sottrarre un intero ad un puntatore significa ottenere un puntatore che punta ad un indirizzo pari all'indirizzo di partenza meno il numero di byte che occupa il tipo puntato moltiplicato per l'intero sottratto.

In termini formali se p è un puntatore ad un tipo tipo:

tipo *p;

tipo è di d byte:

d = sizeof(tipo);

e n è un intero, allora:

p = p - n;

è equivalente a:

\mbox{indirizzo}(p) = \mbox{indirizzo}(p) - n \cdot d

Differenza tra due puntatori

L'ultima operazione aritmetica che possiamo fare con i puntatori è quella di calcolare la differenza tra due puntatori. Questa operazione è possibile solo se i puntatori puntano allo stesso tipo di dato. In questo caso, il risultato sarà un intero che rappresenta il numero di elementi che separano i due puntatori.

Ritornando al caso degli array, se abbiamo un array a di 10 elementi:

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

e due puntatori p e q che puntano rispettivamente agli elementi di indice 3 e 7:

int *p = &a[3];
int *q = &a[7];

possiamo calcolare la differenza tra i due puntatori:

int diff = q - p;

In questo modo, diff sarà uguale a 4. I due puntatori sono rappresentati in figura:

Differenza tra due puntatori
Figura 7: Differenza tra due puntatori

Possiamo verificare il tutto con questo esempio:

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

int *p = &a[3];
int *q = &a[7];

/* Calcoliamo la differenza tra i due puntatori */
int diff = q - p;

/* Stampiamo a schermo il risultato */
printf("Differenza tra i due puntatori: %d\n", diff);

Se proviamo ad eseguire questo stralcio di codice su di una macchina a 64 bit quello che possiamo ottenere è il seguente output:

Differenza tra i due puntatori: 4

Come si può osservare, la differenza tra i due puntatori è uguale a 4.

Definizione

Differenza tra due puntatori

La differenza tra due puntatori, purché puntino allo stesso tipo di dato, è un intero che rappresenta il numero di elementi che separano i due puntatori.

In altre parole il risultato sarà uguale a:

\frac{\mbox{indirizzo}(q) - \mbox{indirizzo}(p)}{d}

dove d è il numero di byte che occupa il tipo puntato.

Sottrarre due puntatori ha senso, come abbiamo visto, se entrambe puntano allo stesso tipo di dato. Inoltre, bisogna prestare attenzione al fatto che la differenza tra due puntatori può essere positiva o negativa. In particolare, se il primo puntatore è maggiore del secondo, la differenza sarà positiva, altrimenti sarà negativa.

Ovviamente, se due puntatori puntano ad array differenti il risultato della sottrazione non è definito.

Comparazione tra puntatori

Un ultimo tipo di operazioni che è possibile effettuare tra puntatori è la comparazione. In particolare, è possibile verificare se due puntatori puntano allo stesso indirizzo, oppure se il primo puntatore è maggiore o minore del secondo.

Questo è possibile attraverso l'utilizzo degli operatori relazionali >, <, >= e <= e attraverso l'utilizzo degli operatori di uguaglianza == e !=.

Ad esempio, se abbiamo un array a di 10 elementi:

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

e due puntatori p e q che puntano rispettivamente agli elementi di indice 3 e 7:

int *p = &a[3];
int *q = &a[7];

possiamo verificare se p è maggiore di q:

if (p > q) {
    printf("p è maggiore di q\n");
} else {
    printf("p non è maggiore di q\n");
}

oppure possiamo verificare se p punta alla stessa posizione di memoria di q:

if (p == q) {
    printf("p punta alla stessa posizione di memoria di q\n");
} else {
    printf("p non punta alla stessa posizione di memoria di q\n");
}
Definizione

Comparazione tra puntatori

Tra due puntatori è possibile utilizzare gli operatori relazionali >, <, >= e <= e gli operatori di uguaglianza == e != per verificare se i due puntatori puntano allo stesso indirizzo, oppure se il primo puntatore è maggiore o minore del secondo.

Tali operazioni sono valide solo se i due puntatori puntano allo stesso tipo di dato.

In Sintesi

In questa lezione abbiamo studiato le operazioni aritmetiche che è possibile effettuare con i puntatori. In particolare, abbiamo visto che è possibile:

  • Sommare un intero ad un puntatore
  • Sottrarre un intero ad un puntatore
  • Calcolare la differenza tra due puntatori

Inoltre, abbiamo visto che è possibile effettuare la comparazione tra puntatori.

Queste operazioni saranno fondamentali per poter lavorare con gli array e con le stringhe.