Lezione 7: Puntatori

Soluzione di Esercizi

Esercizio 6.1 Scrivere una funzione void rot(char C[], int n, int k) che ruota i caratteri del vettore C di k posizioni a destra. Ad esempio, se i caratteri nel vettore C sono "programma" (n = 9) e k = 3, allora la funzione modifica il vettore in modo che contenga "mmaprogra". Scrivere anche un programma che provi la funzione.

#include <stdio.h>

int inputline(char line[], int maxlen) {
    . . .
}

void rot(char C[], int n, int k) {
    char copy[n];
    for (int i = 0 ; i < n ; i++) copy[i] = C[i];
    for (int i = 0 ; i < n ; i++)
        C[(i + k) % n] = copy[i];
}

#define MAX_CHARS     100

// stampa una linea di testo ruotata
int main() {    
    char line[MAX_CHARS];
    printf("Inserire una linea di testo: ");
    int r, n = inputline(line, MAX_CHARS);
    if (n == 0) return 0;
    printf("Inserire la rotazione: ");
    scanf("%d", &r);
    rot(line, n, r);
    for (int i = 0 ; i < n ; i++) 
        printf("%c", line[i]);
    printf("\n");
}

Esercizio 6.2 Scrivere una funzione int included(int A[], int n, int B[], int m) che ritorna 1 se tutti i valori nel vettore A (di dimensione n) sono anche contenuti nel vettore B (di dimensione m), altrimenti ritorna 0. Ad esempio, se A = {2, 1, 3, 2, 1} e B = {5, 1, 3, 2} allora la funzione ritorna 1, se invece B = {5, 3, 2, 0} ritorna 0. Scrivere anche un programma che provi la funzione.

// Ritorna 1 se tutti i valori di A sono inclusi in B, 0 altrimenti
int included(int A[], int n, int B[], int m) {
    for (int i = 0 ; i < n ; i++) {
        int j = 0;
        while (j < m && B[j] != A[i]) j++;
        if (j == m) return 0;
    }
    return 1;
}

Decomposizione in Funzioni

Mostriamo ora un esempio di un programma che può essere convenientemente decomposto in funzioni. Il programma legge due frazioni positive (nel formato numeratore/denominatore) e ne stampa la somma nello stesso formato. Ad esempio, se il programma legge le frazioni 8/15 e 3/10, stampa 25/30. Questo programma si puo' decomporre in una funzione che calcola il massimo comune divisore, una funzione che calcola il minimo comune multiplo e una funzione che legge l'input and calcola la somma.

#include <stdio.h>

// versione lenta: ricerca esaustiva
int mcd(int x, int y) {    
    int d;
    if (x < y) d = x;
    else d = y;
    while ((x % d) != 0 || (y % d) != 0) d--;
    return d;
}

// ritorna il m.c.m. di x e y
int mcm(int x, int y) {    
    int m = mcd(x, y);
    return x*(y/m);
}

int main() {    //calcola la somma di due frazioni
    int num1, den1, num2, den2;
    printf("Inserire due frazioni positive nel formato num/den: ");
    scanf("%d/%d %d/%d", &num1, &den1, &num2, &den2);
    int den = mcm(den1, den2);
    int num = num1*(den/den1) + num2*(den/den2);
    printf("La somma è %d/%d\n", num, den);
}

Per il lettore interessato suggeriamo che il massimo comune divisore si puo' calcolare anche usando l'algoritmo di Euclide che e' estremamente piu' veloce.

// versione veloce: algoritmo di Euclide
/* Assumendo x <= y, il massimo comun divisore di x e y divide 
 * anche il resto della divisione di y per x, cioè, r = y % x. 
 * Infatti, sia d il M.C.D. di x e y, effettuando la divisione 
 * di y per x si ha che y = x*q + r ed essendo sia y che x*q 
 * divisibili per d deve essere che o r è zero (nel qualcaso 
 * d = x) o r > 0 e r è divisibile per d. Quindi, in quest'
 * ultimo caso, il M.C.D. di x e y è uguale al M.C.D. di x e r. */
int mcd_euclide(int x, int y) {    
    if (y < x) {    // assicuriamoci che sia sempre x <= y
        int c = x;
        x = y;
        y = c;
    }
    while (x > 0) {
        int r = y % x;
        y = x;
        x = r;
    }
    return y;
}

Test di Correttezza di Funzioni

Nel caso precedente abbiamo introdotto due funzioni per calcolare la stessa quantita'. Quando si sviluppa software e' consigliato iniziare con funzioni semplici, per poi introdurre versioni piu' complesse, ma tipicamente piu' veloci, dopo avere individuato le parti del programma che causano eccessivi rallentamenti. Per testare se le due versioni calcolano gli stessi valori, non si possono provare tutti le combinazioni di input possibili, ma si possono testare input casuali. Valori pseudo-casuali sono generati in C usando la funzione [rand][rand] definita nell'header stdlib.h.

void test(int n) {
    for(int i = 0; i < n; i ++) {
        int x = max(0,rand());
        int y = max(0,rand());
        if(mcd(x,y) != mcd_lento(x,y)) 
            printf("errore su %d / %d\n", x, y);
    }
}

Puntatori

Con le funzioni definite finora sarebbe impossibile definire una funzione che calcola la somma di due frazione, dato che i tipi definiti fino a questo punto non possono alterare il valore delle variabili della funzione chiamante (con l'eccezione di variabili di tipo array) ne' ritornare piu' valori. Per risolvere questo inconveniente il C introduce il tipo puntatore, che vedremo si puo' utilizzare in molte problematiche di implementazione.

Il valore di una variabile di tipo puntatore e' l'indirizzo di una locazione di memoria, ottenuto tramite l'operatore di indirizzo applicato ad una altra variabile. Il tipo puntatore si dichiara con la seguente sintassi

<tipo>* <nome>;

che dichiara il puntatore <nome> a una variabile di tipo <tipo>. Ad esempio una variabile int* x; e' un puntatore a int. I puntatori si possono usare anche come argomenti di funzioni, ad esempio void f(int* x);. In questo caso, si passa non il valore di una variabile, ma il suo indirizzo in memoria.

Si ottiene un valore di tipo puntatore usando l'operatore di indirizzo. Ad esempio per inizializzare un puntatore si puo' usare la sintassi.

<tipo>* <nome> = &<variabile_di_tipo>;

Ad esempio nell'espressione int* x = &y;, la variabile x e' un puntatore al tipo int, che punta alla locazione in memorie della variabile y.

Se si assegnano valori ad una variabile puntatore ne si cambia la locazione in memoria a cui la variabile punta. Ad esempio,

int* x = &y; // x punta alla locazione della variabile y
x = &z;      // z punta alla locazione della variabile z

Si puo' anche usare i puntatori per leggere o cambiare il contenuto della locazione di memoria puntata, con la sintassi

*<nome> = <espressione>

assegna il valora <espressione> alla locazione di memoria puntata da <nome>. Mentre l'uso dell'espressione

*<nome>

in espressioni aritmetiche accede al valore della memoria all locazione puntata. Ad esempio

int* x = &y; // x punta a y
*x = 5;      // y == 5

Array e Puntatori

Variabili di tipo array sono in realta' puntatori al primo elemento dell'array. Ad esempio per un array <tipo> A[d], l'indirizzo al primo elemento si ottiene semplicemente con <tipo>* ptr = A e' equivalente a <tipo>* ptr = &(A[0]). L'operatore di accesso agli elementi dell'array si puo' usare in modo equivalente attraverso il puntatore, cioe' A[i] ≡ ptr[i].

Quando si passa un array ad una funzione in C, il linguaggio passa implicitamente un puntatore alla prima cella di memoria puntata dall'array. Questa e' la ragione per cui se si alterano gli elementi di un array, se ne altera il contenuto all'uscita della funzione, esattamente con per le variabili puntatori. Questo significa che nella dichiarazione di funzioni il tipo <tipo>[] e' equivalente al tipo <tipo>*.

Esempi

Ecco un esempio di una funzione che prende in input il numeratore e il denominatore di una frazione e la riduce ai minimi termini:

// riduce ai minimi termini la frazione num/den
void minimiTermini(long *num, long *den) {
    long n = *num, d = *den;
    long m = mcd(n, d);
    *num = n/m;
    *den = d/m;
}

Modifica del programma di somma di frazioni in modo che calcoli la somma ridotta:

//calcola la somma di due frazioni, ridotta ai minimi termini
int main() {    
    long num1, den1, num2, den2;
    printf("Inserire due frazioni positive nel formato num/den: ");
    scanf("%ld/%ld %ld/%ld", &num1, &den1, &num2, &den2);
    minimiTermini(&num1, &den1);
    minimiTermini(&num2, &den2);
    long den = mcm(den1, den2);
    long num = num1*(den/den1) + num2*(den/den2);
    minimiTermini(&num, &den);
    printf("La somma è %ld/%ld\n", num, den);
}

Esercizio 7.1 Scrivere una funzione, int parola(char A[], int n, int k, int *plung), che ritorna la posizione della k-esima parola contenuta nell'array A (di dimensione n) e restituisce in *plung la lunghezza di tale parola. Se il numero di parole contenute nell'array A è minore di k, la funzione ritorna -1 e *plung è lasciato invariato. Ad esempio, se A contiene la sequenza di caratteri "La seconda parola" (quindi n = 17) e k = 2, allora la funzione ritorna 3 e in *plung restituisce il valore 7.

Esercizio 7.2 Scrivere un programma (usando la funzione dell'esercizio precedente ed eventualmente altre funzioni) che legge dall'input due linee di testo e stampa tutte le parole della prima linea che appaiono anche nella seconda. Ad esempio, se le due linee di testo sono "Le cose non sono solo cose" e "sono tante cose", allora il programma stampa:

cose
sono
cose