Puntatori a Funzione come Argomenti di Funzione in Linguaggio C

In questa lezione, esploreremo come passare funzioni come argomenti di altre funzioni utilizzando i puntatori a funzione in linguaggio C.

Questa tecnica avanzata permette di creare codice più flessibile e modulare, consentendo di definire comportamenti personalizzati e di implementare strategie diverse all'interno delle funzioni. Comprendere come utilizzare i puntatori a funzione come argomenti è essenziale per sfruttare appieno le potenzialità del linguaggio C e scrivere codice più efficiente e riutilizzabile.

Passare Funzioni come Argomenti di Funzione

Una prima, potentissima, applicazione dei puntatori a funzione che abbiamo studiato nella lezione precedente, è la possibilità di passare funzioni come argomenti di funzione.

Finora, abbiamo scritto funzioni che accettano come argomenti variabili di vario tipo: interi, float, stringhe, array, strutture, ecc. Ma non abbiamo mai scritto una funzione che accetta come argomento un'altra funzione.

Per chiarire il concetto, consideriamo un esempio preso in prestito dalla matematica. Supponiamo di voler realizzare una funzione che calcoli numericamente l'integrale definito della funzione x^3 - 1 dato un certo intervallo [a, b] ed un passo di integrazione dx. La funzione che calcola l'integrale definito è la seguente:

#include <stdio.h>
#include <math.h>

double integrale_x3_meno_1(double a, double b, double dx) {
    double x, integrale = 0;
    for (x = a; x < b; x += dx) {
        integrale += pow(x, 3) - 1;
    }
    return integrale * dx;
}

int main() {
    double a = 0, b = 1, dx = 0.001;
    double integrale = integrale_x3_meno_1(a, b, dx);
    printf("Integrale definito di x^3 - 1 in [%f, %f] = %f\n", a, b, integrale);
    return 0;
}

Proviamo a compilare ed eseguire il programma (Si ricordi che per compilarlo con gcc, dato che usiamo la libreria matematica del C, bisogna aggiungere l'opzione -lm). Il risultato sarà il seguente:

$ gcc integrale_x3_meno_1.c -o integrale_x3_meno_1 -lm
$ ./integrale_x3_meno_1
Integrale definito di x^3 - 1 in [0.000000, 1.000000] = -0.750500

Il risultato è molto simile al risultato vero che è:

\int_{0}^{1} x^3 - 1 \, dx = -\frac{3}{4} = -0.75

Il risultato è molto vicino al risultato vero, ma non è esatto. Questo è dovuto al fatto che stiamo approssimando l'integrale definito con un metodo numerico, ovvero con la somma di Riemann. Più piccolo è il passo di integrazione dx, più preciso sarà il risultato. Ma, ovviamente, più piccolo è il passo di integrazione, più lento sarà il calcolo dell'integrale. Questi dettagli però non sono importanti per il nostro discorso, ma riguardano argomenti di calcolo numerico.

Successivamente, supponiamo di voler calcolare anche l'integrale definito di un'altra funzione, ad esempio x^2 + 1. Per farlo, senza poter passare una funzione come argomento, dovremmo realizzare una seconda funzione:

#include <stdio.h>
#include <math.h>

double integrale_x2_piu_1(double a, double b, double dx) {
    double x, integrale = 0;
    for (x = a; x < b; x += dx) {
        integrale += pow(x, 2) + 1;
    }
    return integrale * dx;
}

int main() {
    double a = 0, b = 1, dx = 0.001;
    double integrale = integrale_x2_piu_1(a, b, dx);
    printf("Integrale definito di x^2 + 1 in [%f, %f] = %f\n", a, b, integrale);
    return 0;
}

Il risultato sarà il seguente:

$ gcc integrale_x2_piu_1.c -o integrale_x2_piu_1 -lm
$ ./integrale_x2_piu_1
Integrale definito di x^2 + 1 in [0.000000, 1.000000] = 1.332834

Il risultato è molto simile al risultato vero che è:

\int_{0}^{1} x^2 + 1 \, dx = \frac{4}{3} \approx 1.333333

Adesso, analizziamo le due funzioni integrale_x3_meno_1 e integrale_x2_piu_1. Le due funzioni sono molto simili, differiscono solo per la funzione che viene integrata. In particolare, la funzione che viene integrata è la parte che cambia.

Se volessimo calcolare l'integrale definito di un'altra funzione, ad esempio x^4 - 2, dovremmo realizzare una terza funzione integrale_x4_meno_2 che è molto simile alle due precedenti. Questo è un esempio di codice ripetuto, che è sempre da evitare.

Grazie ai puntatori a funzione possiamo evitare il codice ripetuto. In particolare, possiamo scrivere una funzione integrale che calcola l'integrale definito di una generica funzione f(x):

#include <stdio.h>
#include <math.h>

/* Prima definiamo le tre funzioni che vogliamo integrare */
double x3_meno_1(double x) {
    return pow(x, 3) - 1;
}

double x2_piu_1(double x) {
    return pow(x, 2) + 1;
}

double x4_meno_2(double x) {
    return pow(x, 4) - 2;
}

/* Definiamo la funzione che calcola l'integrale definito */
double integrale(double a, double b, double dx, double (*f)(double)) {
    double x, integrale = 0;
    for (x = a; x < b; x += dx) {
        integrale += f(x);
    }
    return integrale * dx;
}

int main() {
    double a = 0, b = 1, dx = 0.001;
    double integrale1 = integrale(a, b, dx, x3_meno_1);
    printf("Integrale definito di x^3 - 1 in [%f, %f] = %f\n", a, b, integrale1);
    double integrale2 = integrale(a, b, dx, x2_piu_1);
    printf("Integrale definito di x^2 + 1 in [%f, %f] = %f\n", a, b, integrale2);
    double integrale3 = integrale(a, b, dx, x4_meno_2);
    printf("Integrale definito di x^4 - 2 in [%f, %f] = %f\n", a, b, integrale3);
    return 0;
}

In questo modo abbiamo definito una generica funzione integrale che è in grado di calcolare l'integrale definito di una generica funzione f(x). La funzione integrale accetta come argomento una funzione f che rappresenta la funzione da integrare. In questo modo, possiamo calcolare l'integrale definito di qualsiasi funzione f(x) senza dover scrivere una funzione ad hoc per ogni funzione che vogliamo integrare. Tutto grazie ai puntatori a funzione.

Come si può notare, la sintassi per usare un puntatore a funzione come argomento è identica a quella per definire un puntatore a funzione. In particolare, la sintassi è la seguente:

tipo_ritorno funzione(tipo_ritorno_f (*nome_puntatore)(tipo_argomento_f));

Il nome del parametro è il nome del puntatore a funzione: nome_puntatore.

Ricapitolando:

Definizione

Passare Funzioni come Argomenti di Funzione

Una funzione può essere passata come argomento ad un'altra funzione attraverso i puntatori a funzione.

Una funzione che accetta come parametro un puntatore a funzione ha la seguente sintassi:

tipo_ritorno funzione(..., tipo_ritorno_f (*nome_puntatore)(tipo_argomento_f), ...) {
    ...
}

La funzione qsort

Un'interessante applicazione delle funzioni passate come argomento riguarda la funzione della libreria standard del C qsort.

La funzione qsort è una funzione di libreria definita nell'header <stdlib.h> ed è in grado di ordinare un array di elementi qualunque. Si tratta di una funzione molto generica e potente che richiede in ingresso proprio un puntatore a funzione. Vediamo perché.

Dal momento che gli elementi dell'array che passiamo come argomento a qsort possono essere di un tipo qualunque, ad esempio anche strutture dati, qsort non può sapere come confrontare due elementi dell'array. Per fare ciò, qsort richiede un puntatore a funzione che rappresenta la funzione di confronto tra due elementi. Questa funzione di confronto deve poter determinare, dati due elementi, quale dei due sia il più piccolo. Per questo, questa funzione che dobbiamo fornire noi prende il nome di funzione di confronto.

Tale funzione, prende in ingresso due puntatori a due elementi dell'array e restituisce un intero che rappresenta il risultato del confronto. In particolare, la funzione di confronto deve restituire:

  • un valore negativo se il primo elemento è minore del secondo,
  • un valore positivo se il primo elemento è maggiore del secondo,
  • zero se i due elementi sono uguali.

Il prototipo della funzione qsort è il seguente:

void qsort(void *base,
           size_t nmemb,
           size_t size,
           int (*compar)(const void *, const void *));

Dove:

  • base è il puntatore all'array da ordinare,
  • nmemb è il numero di elementi dell'array,
  • size è la dimensione in byte di ciascun elemento dell'array,
  • compar è il puntatore alla funzione di confronto.

Proviamo ad applicare la funzione qsort ad un primo caso semplice: ordinare un array di double in ordine crescente. Per fare ciò, dobbiamo scrivere una funzione di confronto che confronti due double e restituisca un valore negativo se il primo è minore del secondo, un valore positivo se il primo è maggiore del secondo, e zero se i due sono uguali.

#include <stdio.h>
#include <stdlib.h>

/* Funzione di Confronto per double */
int confronto_double(const void *a, const void *b) {
    double x = *(double *)a;
    double y = *(double *)b;
    if (x < y) return -1;
    if (x > y) return 1;
    return 0;
}

int main() {
    double array[] = {3.14, 2.71, 1.41, 1.61, 1.73};
    size_t n = sizeof(array) / sizeof(array[0]);
    qsort(array, n, sizeof(double), confronto_double);
    for (size_t i = 0; i < n; i++) {
        printf("%f ", array[i]);
    }
    printf("\n");
    return 0;
}

Il risultato sarà il seguente:

$ gcc qsort_double.c -o qsort_double
$ ./qsort_double
1.410000 1.610000 1.730000 2.710000 3.140000

Il risultato è corretto: l'array è stato ordinato in ordine crescente.

Se volessimo ordinare l'array in ordine decrescente, possiamo scrivere una funzione di confronto che restituisca il valore opposto:

int confronto_double_decrescente(const void *a, const void *b) {
    return -confronto_double(a, b);
}

E, successivamente, invocare qsort con la funzione di confronto confronto_double_decrescente:

qsort(array, n, sizeof(double), confronto_double_decrescente);

Il risultato sarà il seguente:

$ gcc qsort_double_decrescente.c -o qsort_double_decrescente
$ ./qsort_double_decrescente
3.140000 2.710000 1.730000 1.610000 1.410000

Questa grande flessibilità è ciò che rende i puntatori a funzione molto potenti.

Possiamo riutilizzare la funzione qsort anche su array di tipi completamente diversi. Supponiamo, ad esempio, di voler realizzare un programma che ordina un array di stringhe in ordine alfabetico. In questo caso, dobbiamo scrivere una funzione di confronto che confronti due stringhe e restituisca un valore negativo se la prima è minore della seconda, un valore positivo se la prima è maggiore della seconda, e zero se le due sono uguali.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Funzione di Confronto per stringhe */
int confronto_stringhe(const void *a, const void *b) {
    const char *x = *(const char **)a;
    const char *y = *(const char **)b;
    return strcmp(x, y);
}

int main() {
    const char *array[] = {"cane", "gatto", "topo", "elefante", "formica"};
    size_t n = sizeof(array) / sizeof(array[0]);
    qsort(array, n, sizeof(char *), confronto_stringhe);
    for (size_t i = 0; i < n; i++) {
        printf("%s ", array[i]);
    }
    printf("\n");
    return 0;
}

Il risultato sarà il seguente:

$ gcc qsort_stringhe.c -o qsort_stringhe
$ ./qsort_stringhe
cane elefante formica gatto topo

Per ordinare le stringhe in ordine decrescente, basta scrivere la funzione di confronto in questo modo:

int confronto_stringhe_decrescente(const void *a, const void *b) {
    const char *x = *(const char **)a;
    const char *y = *(const char **)b;
    /* Inverte il valore di strcmp */
    return -strcmp(x, y);
}

Vediamo un ultimo esempio. Supponiamo di avere un array di strutture Persona, dove ogni elemento è così composto:

struct Persona {
    char nome[20];
    char cognome[20];
    int eta;
};

Vogliamo realizzare un programma che ordini, dapprima, le persone in ordine alfabetico del cognome. Poi vogliamo ordinare le persone in ordine crescente dell'età. Per fare ciò, dobbiamo scrivere due funzioni di confronto: una per il cognome e una per l'età.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct Persona {
    char nome[20];
    char cognome[20];
    int eta;
};

/* Funzione di Confronto per cognome */
int confronto_cognome(const void *a, const void *b) {
    const struct Persona *x = (const struct Persona *)a;
    const struct Persona *y = (const struct Persona *)b;
    return strcmp(x->cognome, y->cognome);
}

/* Funzione di Confronto per età */
int confronto_eta(const void *a, const void *b) {
    const struct Persona *x = (const struct Persona *)a;
    const struct Persona *y = (const struct Persona *)b;
    return x->eta - y->eta;
}

int main() {
    struct Persona array[] = {
        {"Mario", "Rossi", 30},
        {"Luca", "Bianchi", 25},
        {"Paolo", "Verdi", 35},
        {"Giuseppe", "Neri", 20},
        {"Giovanni", "Gialli", 40}
    };
    size_t n = sizeof(array) / sizeof(array[0]);
    qsort(array, n, sizeof(struct Persona), confronto_cognome);
    for (size_t i = 0; i < n; i++) {
        printf("%s %s %d\n", array[i].nome, array[i].cognome, array[i].eta);
    }
    printf("\n");
    qsort(array, n, sizeof(struct Persona), confronto_eta);
    for (size_t i = 0; i < n; i++) {
        printf("%s %s %d\n", array[i].nome, array[i].cognome, array[i].eta);
    }
    return 0;
}

Il risultato sarà il seguente:

$ gcc qsort_persona.c -o qsort_persona
$ ./qsort_persona
Luca Bianchi 25
Giovanni Gialli 40
Giuseppe Neri 20
Mario Rossi 30
Paolo Verdi 35

Giuseppe Neri 20
Luca Bianchi 25
Mario Rossi 30
Paolo Verdi 35
Giovanni Gialli 40

Come si può notare, le persone sono state ordinate prima per cognome e poi per età.

Ricapitolando:

Definizione

La Funzione qsort della Libreria Standard del C

La funzione qsort della libreria standard del C è in grado di ordinare un array di elementi qualunque. Per fare ciò, richiede un puntatore a funzione che rappresenta la funzione di confronto tra due elementi.

Il prototipo della funzione qsort è il seguente:

#include <stdlib.h>

void qsort(void *base,
           size_t nmemb,
           size_t size,
           int (*compar)(const void *, const void *));

Dove:

  • base è il puntatore all'array da ordinare,
  • nmemb è il numero di elementi dell'array,
  • size è la dimensione in byte di ciascun elemento dell'array,
  • compar è il puntatore alla funzione di confronto.

La funzione di confronto deve avere il seguente prototipo:

int compar(const void *a, const void *b);

La funzione di confronto deve restituire:

  • un valore negativo se il primo elemento è minore del secondo,
  • un valore positivo se il primo elemento è maggiore del secondo,
  • zero se i due elementi sono uguali.

In Sintesi

In questa lezione abbiamo visto come passare funzioni come argomenti di funzione grazie ai puntatori a funzione. Questa tecnica è molto potente e ci permette di scrivere codice più generico e flessibile. Abbiamo visto due esempi di applicazione dei puntatori a funzione: il calcolo dell'integrale definito di una generica funzione e l'ordinamento di un array di elementi qualunque.

In particolare, abbiamo visto che:

  • Una funzione può essere passata come argomento ad un'altra funzione attraverso i puntatori a funzione.
  • La funzione qsort della libreria standard del C è in grado di ordinare un array di elementi qualunque. Per fare ciò, richiede un puntatore a funzione che rappresenta la funzione di confronto tra due elementi.