Lezione 6: Funzioni

Soluzione di Esercizi

Esercizio 5.1 Scrivere un programma che legge una linea di testo e per ogni carattere alfabetico che è l'iniziale di una parola stampa il numero di parole che iniziano con quel carattere.

#include <stdio.h>
#include <ctype.h>

// stampa il numero di parole che hanno la stessa iniziale
int main() {    
    printf("Inserire una linea di testo: ");
    int c, inizio = 0;
    int iniziali[26] = {0};
    do {
        c = getchar();
        if (isalpha(c)) {
            if (!inizio) {
                iniziali[tolower(c) - 'a']++;
                inizio = 1;
            }
        } else inizio = 0;
    } while (c != '\n');
    for (int i = 0 ; i < 26 ; i++)
        if (iniziali[i] > 0) 
            printf("%c: %d\n", 'a' + i, iniziali[i]);
}

Nella soluzione sopra abbiamo usato la funzione isalpha testa se un carattere e' alfabetico, mentre la funzione tolower converte un carattere alla lettera minuscola. Queste funzioni sono dichiarate nell'header ctype.h, come discuteremo in seguito.

Esercizio 5.2 Scrivere un programma che legge un intero n e poi legge n numeri in virgola mobile e se ci sono due numeri la cui differenza (in valore assoluto) è minore di 0.001 stampa "Ci sono numeri vicini", altrimenti stampa "NON ci sono numeri vicini". Ad esempio, se i numeri letti sono 1.03, 0.056, 1.0305, allora il programma stampa "Ci sono numeri vicini".

#include <stdio.h>

// determina se in una sequenza di numeri decimali ci sono numeri "vicini"
int main() {    
    int n;
    printf("Quanti numeri? ");
    scanf("%d", &n);
    float numeri[n];
    for (int i = 0 ; i < n ; i++) {
        printf("%2d: ", i+1);
        scanf("%f", &numeri[i]);
    }
    for (int i = 0 ; i < n ; i++)
        for (int j = i + 1 ; j < n ; j++) {
            float d = numeri[i] - numeri[j];
            if (d < 0) d = -d;
            if (d < 0.001) {
                printf("Ci sono numeri vicini\n");
                return 0;
            }
        }   
    printf("NON ci sono numeri vicini\n");
}

Funzioni

Nei programmi precedenti abbiamo sviluppato molte applicazioni che si potrebbero riutilizzare il altri programmi. Al momento, l'unico modo in cui possiamo farlo e' di tagliare ed incollare il codice in un altro programma, ma la cosa e' prona a molti errori. Il modo piu' semplice per riutilizzare il codice e' di isolare parti di codice in funzioni definite dall'utente. La sintassi per dichiarare una funzione e'

<tipo> <nome>(<lista_parametri>) {
    <istruzioni>
}

dove la lista dei parametri e' costituita da zero o piu' dichiarazioni di "variabili" separate da virgole, con la sintassi

<lista_parametri> ≡ <tipo_1> <nome_1>, <tipo_2> <nome_2>, ...

Il tipo di ritorno della funzione e' il tipo che la funzione ritorna alla fine della sua esecuzione, usando l'istruzione

return <expressione>;

Se una funzione non ritorna nessun valore, si usa il tipo void come tipo del valore di ritorno. Il corpo della funzione e' la lista di istruzioni che vengono eseguite quando si esegue la funzione.

Nota che nei programmi precedenti abbiamo definito una funzione particolare, main, che il C usa come il "punto di entrata" del programma. La funzione main che abbiamo definito non prende nessun argomento e non ritorna alcun valore.

Per eseguire la funzione, si chiama la funzione usando la seguente sintassi:

<nome>(<espressione_1>, <espressione_2>, ...)

Quando la funzione viene chiamata, l'esecuzione del programma salta alle istruzioni che costituiscono il corpo della funzione. Prima della chiamata, il valore dei parametri della funzione sono definiti dai valori delle espressione passate come argomenti durante la chiamata. L'esecuzione della funzione continua fino a quando una istruzione di tipo return e' incontrata o la lista delle istruzioni ha termine. Nel primo caso, l'esecuzione della funzione viene interrotta immediatamente e il valore passato a return viene ritornato al punto di chiamata.

Esempi

Funzione che ritorna la terza potenza di un numero in virgola mobile.

float pow3(float x) {
    return x*x*x;
}

Funzione che ritorna il massimo di due interi.

int max(int a, int b) {
    if (a > b) return a;
    else return b;
}

Passaggio di Parametri

I parametri di tipo numerico (intero e virgola mobile) vengono passati per valore alle funzioni. Questo significa che se il parametro viene cambiato durante l'esecuzione della funzione, questo non ha effetto al di fuori della funzione stessa. I parametri di tipo array sono invece passati per riferimento. Questo significa che se i valori delle componenti dell'array sono alterati durante l'esecuzione della funzione, gli stessi valori sono alterati nell'array che viene passato come argomento durante la chiamata.

Esempi

Funzione che ritorna la somma dei valori di un array.

float sum(float[] v, int n) {
    float s = 0;
    for(int i = 0; i < n; i ++) s += v[i];
    return s;
}

Funzione che imposta gli elementi di un array ad un valore dato.

void set(float[] v, int n, float value) {
    for(int i = 0; i < n; i ++) v[i] = value;
}

Benefici dell'uso di Funzioni

L'uso di funzioni e' fondamentale nello sviluppo del software per i seguenti motivi:

Librerie di Funzioni

Il C permette permette di definire funzioni non solo nel file del programma, ma anche in file addizionali che vengono compilati insieme ad programma principale o usati per definire funzioni. Organizzare un programma largo usando una molteplicita' di files e' utile per le stesse ragioni per cui e' utile usare funzioni. Infatti possiamo pensare all'organizzazione su piu' files come un ulteriore metodo di partizione del programma.

Quando una funzione definita in un altro file, deve essere chiamata, il codice chiamante deve conoscere il prototipo della funzione, cioe' il nome, valore di ritorno e gli argomenti della funzione (omettendo il corpo della funzione definito nel file addizionale). Il prototipo di una funzione di dichiara come

<tipo> <nome>(<lista_parametri>);

ed e' generalmente incluso il file aggiuntivi a se stanti. Ad esempio, e' convenzione che se la definizione di una funzione si trova nel file file.c, il prototipo della funzione e' definito nel file file.h, detto l'header file.

Direttive di preprocessore

I prototipi definiti negli headers sono inclusi nel programma usando la direttiva

#include "file_utente.h"
#include <file_standard.h>

Questa direttiva viene processata prima della compilazione da un programma detto preprocessore che copia letteralmente il contenuto dei files .h nella posizione della direttiva #include. Esistono due modi per specificare i files da includere. Se si usa la versione con "file.h" il preprocessore cerca il file nella cartella attuale. Se invece si usa la versione <file.h> il preprocessore cerca il file nelle cartelle di sistema.

Un'altra direttiva di uso comune e' quella usata per definire costanti:

#define <nome> <valore>

Quando il preprocessore incontra questa direttiva sostituisce tutte le occorrenze di <nome> trovate nel programma con il <valore> specificato.

Esempi

Esempio di una funzione che legge dallo standard input una sequenza di caratteri e li scrive nell'array line fino a che non legge EOF (end-of-file) o il carattere '\n' o sono stati letti maxlen caratteri; la funzione ritorna il numero di caratteri letti.

/* Legge dallo standard input nell'array line una sequenza di caratteri 
 * fino a quando incontra EOF, il carattere di fine linea '\n' (escluso) 
 * o ha letto maxlen caratteri. Ritorna il numero di caratteri letti. 
 * L'array deve avere dimensione almeno pari a maxlen. */
int inputline(char line[], int maxlen) {
    int c, n = 0;
    do {
        c = getchar();
        if (c != EOF && c != '\n')
            line[n++] = c;
    } while (c != EOF && c != '\n' && n < maxlen);
    return n;
}

La funzione si puo' dichiarare con prototipo

int inputline(char line[], int maxlen);

Schema di un programma che legge una linea di testo (usando la funzione inputline()) e determina se nella linea di testo ci sono due parole uguali usando una opportuna funzione per determinare se due parole, che iniziano in posizioni date, sono uguali. Lasciamo il completamento come esercizio.

#include <stdio.h>
#include <ctype.h>

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

/* Ritorna true se nelle posizioni i e j dell'array line di lunghezza n 
 * iniziano delle parole e queste sono uguali. 
 * Si assume che 0 <= i < n, 0 <= j < n. */
int eqwords(char line[], int n, int i, int j) {
    if (!isalpha(line[i]) || (i > 0 && isalpha(line[i - 1])))
        return 0;
    if (!isalpha(line[j]) || (j > 0 && isalpha(line[j - 1])))
        return 0;
    int eq = -1;    //1: uguali; 0: diverse; -1: indefinito
    while (eq == -1) {
        if (i < n && isalpha(line[i])) {         //Se la prima parola non
                                                 //è ancora terminata
            if (j >= n || !isalpha(line[j]) || line[j] != line[i])
                eq = 0;
        } else if (j < n && isalpha(line[j])) {  //Se la prima parola è 
                                                 //terminata e la seconda
            eq = 0;                              //ancora non è terminata
        } else            //Se le due parole sono terminate
            eq = 1;
        i++;
        j++;
    }
    return (eq == 1);
}

#define MAXLEN    1000    // lunghezza massima della linea di testo

// determina se ci sono parole ripetute in una linea di testo
int main() {    
    char line[MAXLEN];
    printf("Inserire una linea di testo: ");
    int n = inputline(line, MAXLEN);
    for (int i = 0 ; i < n ; i++) {
        for (int j = i + 1 ; j < n ; j++) {
            if (eqwords(line, n, i, j)) {
                printf("Parole ripetute in posizioni %d e %d\n", i, j);
                return 0;
            }
        }
    }
    printf("Non ci sono parole ripetute\n");
}

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.

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.