Lezione 13: Aritmetica dei Puntatori

Soluzioni di Esercizi

Esercizio 12.1 Scrivere una funzione ricorsiva int binarysearchIns(int A[], int i, int j, int x) che esegue una ricerca binaria nell'array ordinato A, tra gli indici i e j, del valore x. Se x è presente ritorna l'indice in cui si trova, altrimenti ritorna l'indice della posizione in cui dovrebbe essere inserito per mantenere l'array ordinato. Ad esempio, se A = {2, 4, 7, 9}, x = 5, i = 0 e j = 3, la funzione deve ritornare 2.

/* Assumendo che l'array A è ordinato in senso crescente, ritorna 
 * l'indice del valore x nel sottoarray A[i, j], se non è presente
 * ritorna l'indice in cui dovrebbe essere inserito il valore x 
 * per mantenere l'array ordinato. */
int bsearchIns(int A[], int i, int j, int x) {
    if (j < i) 
        return i;
    else if (i == j) 
        return (x <= A[i] ? i : i + 1);
    else {
        int k = (i + j)/2;
        if (A[k] == x) return k;
        else if (x > A[k]) return bsearchIns(A, k+1, j, x);
        else return bsearchIns(A, i, k-1, x);
    }
}

Esercizio 12.2 Scrivere una funzione ricorsiva int pal(char C[], int n) che ritorna vero se l'array di n caratteri è palindromo, cioè, rimane lo stesso se letto da sinistra a destra o da destra a sinistra. Ad esempio, se C = {'r','o','t','o','r'}, ritorna vero; se invece C = {'r','o','t','o','r','e'} ritorna falso.

// ritorna vero se l'array C di n caratteri è palindromo
int pal(char C[], int n) {
    if (n <= 1) return 1;
    else if (C[0] != C[n - 1]) return 0;
    else return pal(&C[1], n - 2);
}

Aritmetica dei Puntatori

Per i tipi puntatori, escluso il puntatore a void, il C definisce le operazioni di somma e difference tra puntatori e numeri interi. Se k e' un intero e ptr e' un puntare al tipo <tipo>, ptr + k e' un puntatore allo stesso tipo che punta alla locazione di memoria che e' k*sizeof(<tipo>) bytes successiva rispetto a ptr, mentre ptr - k e' un puntatore similmente definito ma precedente. In memoria, questo e' rappresentato come

     sizeof(<tipo>)
    /              \
... -------------------------------      ...      ----------------- ...
    |              |              |               |               |  
... -------------------------------      ...      ----------------- ...
    ptr            ptr + 1        ptr + 2         ptr + k

Ad esempio, se dichiariamo un array int A[3] = {1,3,5} e un puntatore int* ptr = A, e il primo elemento dell'array ha indice 2048 in memoria, la rappresentazione in memoria e'

     sizeof(int) == 4
    /                \
... ------------------------------------------------------ ...
    |        1       |        3        |        5        |    
... ------------------------------------------------------ ...
    ptr == 2048      ptr+1 == 2056     ptr+2 == 2060           

Il C definisce anche un operatore di differenza tra puntatori dello stesso tipo, ma non void. In particolare, ptr1 - ptr2 e' il numero di elementi di tipo <tipo> tra ptr1 e ptr2.

Infine il C supporta la comparazione tra puntatori dello stesso tipo definendo tutti gli operatori relazionali tra due puntatori dello stesso tipo.

Esempi

Implementazione di una funzione che copia un blocco di n bytes puntato da src nel blocco puntato da dst (si assume che i blocchi siano disgiunti). Notate che string.h dichiara questa funzione, ma con implementazione molto piu' veloce.

void *memcpy(void *dst, const void *src, long n) {
    char *d = dst;          //char corrisponde ad un byte
    const char *s = src;
    while (n > 0) {
        *(d++) = *(s++);
        n--;
    }
    return d;
}

Array e Puntatori

Come discusso in precedenza, i puntatori e i tipi array hanno molte similarita' in comune. Introdurremo ora una equivalenza fra l'aritmetica dei puntatori e l'operazione di accesso agli elementi. In particolare, per ogni tipo non void, e per l'array <tipo> array[<dimensione>] e il puntatore <tipo>* ptr = array, sono vere le seguenti equivalenze:

ptr[i]  ≡  *(ptr + i)  ≡  array[i]  ≡  *(array + i)
&(ptr[i])  ≡  ptr + i  ≡  &(array[i])  ≡  array + i

A questo punto potrebbe sembrare che i due tipi, <tipo>* e <tipo> [dimensione], siamo identici. Nella maggioranza dei casi si possono considerare equivalenti, ed infatti ci sono conversioni automatiche da array a puntatore, con due eccezioni. Il primo caso e' che l'operatore sizeof ritorna la dimensione del tipo, cioe'

sizeof(<tipo>*) == 8 // nella maggioranza delle macchine
sizeof(<tipo> [<dimensione>]) == sizeof(<tipo>)*<dimensione>

Il secondo caso e' che l'operatore & ritorna lo stesso indirizzo, ma con tipi diversi. Nel caso dei puntatori, &ptr e' di tipo <tipo>** cioe' puntatore a puntatore a <tipo>. Nel caso degli array &array e' di tipo <tipo> (*)[<dimensione>] cioe' puntatore ad un array di tipo <tipo> [<dimensione]. Ad esempio, per un array int A[3], la rappresentazione in memoria di questo secondo caso e'

     sizeof(int) == 4
    /                \
... ------------------------------------------------------ ...
    |                |                 |                 |    
... ------------------------------------------------------ ...
    A == 2048        A+1 == 2056       A+2 == 2060           
   &A                                                    (&A)+1=2048+12
    \                                                    /
     -------------- sizeof(int [3]) == 12 --------------- 

Array Multidimensionali e Puntatori

Una simile interpretazione e' possibile per gli array multidimensionali. Consideriamo l'array <tipo> array[<d1>][<d2>], che si puo' pensare come un array di <d1> elementi ognuno di tipo <tipo> [<d2>]. Questa interpretazione e' avvalorata dal tipo di array[i] che e' <tipo> (*)[<d2>]. Ad esempio per int M[3][4], la rappresentazione in memoria e'

. . . ------------------------------------- . . .
      |  |  |  |  |  |  |  |  |  |  |  |  |
. . . ------------------------------------- . . .
      |           |           |
     M[0]        M[1]        M[2]
      M

Come nel caso precedente ci sono equivalenze tra l'operatore di accesso agli elementi di un array e l'aritmetica dei puntatori. In particolare:

array[i] ≡ *(array + i)

Nella espressione precedente array è convertito ad un puntatore di tipo <tipo> (*)[<d2>], quindi array+1 è l'indirizzo della seconda riga della matrice e *(array+1) è l'indirizzo del primo elemento della seconda riga. Inoltre

array + i == array[i]

ma non equivalente, dato che array + i e array[i] hanno lo stesso valore, ma array+1 ha tipo <tipo> (*)[<d2>] mentre M[1] ha tipo <tipo> *. In generale,

array[i][j]  ≡  *(array[i]+j)  ≡  *((*(array+i))+j)

Allocazione Dinamica di Array Multidimensionali

Se si vuole allocare dinamicamente una matrice in modo tale che si possa usare la sintassi delle parentesi quadre è necessario allocare un array che contiene i puntatori alle righe (cioè, al primo elemento di ogni riga) e poi allocare le righe. La funzioni nel seguito allocano e deallocano matrici.

#include <stdlib.h>

// alloca una matrice con nr righe e nc colonne
int **allocmatrix(int nr, int nc) {    
    int  **M = malloc(nr*sizeof(int *));
    for (int i = 0 ; i < nr ; i++)
        M[i] = malloc(nc*sizeof(int));
    return M;
}

/* Libera la memoria della matrice M, precedentemente allocata 
 * tramite allocmatrix() */
void freematrix(int **M, int nr) {    
    for (int i = 0 ; i < nr ; i++) free(M[i]);
    free(M);
}

Notate che una matrice allocata dinamicamente potrebbe avere righe di lunghezza differente (ad es. una matrice triangolare). Inoltre, l'intera matrice potrebbe essere allocata in un unico blocco.

Esercizi

Esercizio 13.1 Scrivere una funzione int **alloctrimatrix(int n, int val) che ritorna una matrice triangolare di interi, allocata dinamicamente, riempita con il valore val. Ad esempio, se n = 5 e val = 2 la matrice allocata deve essere:

2 2 2 2 2
2 2 2 2
2 2 2
2 2
2

Esercizio 13.2 Scrivere una funzione void freetrimatrix(int **M, int n) che disalloca la matrice M, precedentemente allocata tramite la funzione alloctrimatrix().