Puntatori come Argomenti di Funzioni in Linguaggio C

Il primo metodo di impiego dei puntatori in linguaggio C è quello di passarli come argomenti di funzioni: il passaggio di parametri per riferimento.

Usando questa tecnica, è possibile passare ad una funzione l'indirizzo di una variabile e, nel corpo della funzione, modificare il contenuto della variabile stessa.

Passaggio di Puntatori come argomento di Funzioni

Abbiamo visto nelle lezioni sulle funzioni in linguaggio C che il passaggio di variabili e valori avviene per copia.

In altre parole, quando passiamo una variabile come argomento di una funzione il compilatore provvede a creare una copia della variabile che verrà utilizzata internamente alla funzione.

Ad esempio, prendiamo la funzione che segue:

int test(int a, int b) {
    a = a - 2;
    return b * a;
}

Questa funzione prende in ingresso due numeri interi, a e b, e restituisce il risultato di (a - 2) \cdot b.

Se invochiamo la funzione in questo modo:

int x = 4;
int y = 6;
int z;

z = test(x, y);

Abbiamo che nel momento dell'invocazione a sarà una copia di x. Motivo per cui, quando verrà eseguita la riga a = a - 2, il contenuto di x non sarà modificato.

Questo comportamento permette di proteggere gli argomenti passati alle funzioni da possibili modifiche. Tuttavia limita le possibilità di applicazione delle funzioni.

Supponiamo di voler scrivere un programma che prende in ingresso un angolo espresso in gradi con parte decimale, ad esempio 132.453°, e che stampi in uscita lo stesso angolo ma espresso in gradi, primi e secondi, ad esempio 132° \, 27' \, 10.8''.

Iniziamo a scrivere lo scheletro del programma in questo modo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main() {
    double angolo = 0.0;
    int gradi = 0;
    int primi = 0;
    int secondi = 0;

    printf("Inserisci l'angolo: ");
    scanf("%f\n", &angolo);

    /* Elaborazione */
    /* ... */

    printf("Il risultato è: %d° %d' %d''\n", gradi, primi, secondi);
    return 0;
}

Dobbiamo riempire le righe 12 e 13 con le istruzioni necessarie per il calcolo delle variabili gradi, primi e secondi a partire dalla variabile angolo.

Potremmo pensare di scrivere una funzione, ad esempio converti_angolo ma abbiamo un problema. Una funzione può restituire un solo valore di ritorno, ossia un solo risultato. Nel nostro caso, invece, i risultati sono tre.

Si potrebbe decidere di scrivere tre funzioni, ognuna per calcolare gradi, primi e secondi rispettivamente. Oppure si possono usare i puntatori.

Infatti, possiamo scrivere una funzione che calcola le tre variabili passando ad essa i riferimenti, ossia gli indirizzi, delle tre variabili a tre puntatori. La firma della funzione diventerebbe:

void converti_angolo(double angolo, int *gradi, int *primi, int *secondi);

A questo punto possiamo modificare il programma in questo modo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>

int main() {
    double angolo = 0.0;
    int gradi = 0;
    int primi = 0;
    int secondi = 0;

    printf("Inserisci l'angolo: ");
    scanf("%f\n", &angolo);

    converti_angolo(angolo, &gradi, &primi, &secondi);

    printf("Il risultato è: %d° %d' %d''\n", gradi, primi, secondi);
    return 0;
}

Nella riga 12 stiamo invocando la funzione converti_angolo passando come argomento gli indirizzi delle tre variabili gradi, primi e secondi. A questo punto, nel corpo della funzione i tre parametri puntatori saranno degli alias in tutto e per tutto delle variabili argomento.

Per cui:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <math.h>

void parte_intera_e_frazionaria(double valore,
        double *parte_intera,
        double *parte_frazionaria) {
    *parte_intera = floor(valore);
    *parte_frazionaria = valore - *parte_intera;
}

void converti_angolo(double angolo, int *gradi, int *primi, int *secondi) {
    double parte_intera;
    double parte_frazionaria;

    parte_intera_e_frazionaria(angolo, &parte_intera, &parte_frazionaria);

    *gradi = (int) parte_intera;

    angolo = parte_frazionaria * 60.0;

    parte_intera_e_frazionaria(angolo, &parte_intera, &parte_frazionaria);

    *primi = (int) parte_intera;

    angolo = parte_frazionaria * 60.0;

    parte_intera_e_frazionaria(angolo, &parte_intera, &parte_frazionaria);

    *secondi = (int) parte_intera;
}

int main() {
    double angolo = 0.0;
    int gradi = 0;
    int primi = 0;
    int secondi = 0;

    printf("Inserisci l'angolo: ");
    scanf("%lf", &angolo);

    converti_angolo(angolo, &gradi, &primi, &secondi);

    printf("Il risultato è: %d° %d' %d''\n", gradi, primi, secondi);
    return 0;
}

Nel codice di sopra, i parametri gradi, primi e secondi sono puntatori ad interi. Passando alla funzione, nella riga 41 gli indirizzi delle variabili abbiamo che, una volta terminata l'esecuzione della funzione, tali variabili conterranno il valore finale calcolato. In altre parole, con questa tecnica abbiamo ottenuto più valori di ritorno.

Una possibile esecuzione del programma è la seguente:

Inserisci l'angolo: 132.453
Il risultato è: 132° 27' 10''

Attraverso i puntatori è quindi possibile passare gli argomenti di una funzione per riferimento.

Definizione

Passaggio di argomenti per riferimento

Attraverso l'utilizzo di puntatori è possibile passare gli argomenti di una funzione per riferimento. In altre parole, passando un puntatore come argomento di una funzione si passa l'indirizzo del dato a cui la funzione stessa può accedere. In questo modo il dato può essere modificato nel corpo della funzione.

Questa tecnica risulta utile soprattutto quando si vuole realizzare una funzione che restituisca più di un risultato.

Il passaggio di puntatori come argomento di funzione in realtà non è una cosa nuova. Infatti l'abbiamo già adoperato nel momento in cui abbiamo utilizzato la funzione scanf.

Ad esempio, supponendo di voler leggere un numero intero da console possiamo scrivere il codice che segue:

int x;
scanf("%d", &x);

Quando usiamo la funzione scanf dobbiamo sempre passare l'indirizzo della variabile di destinazione che conterrà il valore letto da riga di comando. Per tal motivo, nell'esempio di sopra abbiamo utilizzato l'operatore indirizzo davanti alla variabile x nell'invocare la funzione scanf.

Nota

Attenzione al passaggio di puntatori come argomento di funzione

Quando si utilizzano i puntatori nel passaggio di argomenti a funzione bisogna prestare la massima attenzione. Infatti, passare un argomento che non sia un puntatore ad una funzione che se ne aspetta uno potrebbe avere conseguenze impredicibili.

Ritornando all'esempio della conversione di un angolo in gradi abbiamo la funzione converti_angolo la cui firma è la seguente:

void converti_angolo(double angolo, int *gradi, int *primi, int *secondi)

La funzione si aspetta tre puntatori ad intero come secondo, terzo e quarto parametro. Provando a invocare la funzione alla riga 41 in questo modo, cioè senza passare l'indirizzo delle tre variabili ma passando il valore stesso:

converti_angolo(angolo, gradi, primi, secondi);

La funzione, in tal caso, proverebbe a salvare il valore di gradi, primi e secondi nell'indirizzo indicato da gradi, primi e secondi. Proverebbe, cioè, a modificare il contenuto della memoria puntata dai valori di gradi, primi e secondi. Tali locazioni sono sconosciute e le conseguenze potrebbero essere disastrose.

In generale, i compilatori C, di fronte a tali situazioni, forniscono dei messaggi di warning. Ad esempio, il compilatore gcc restituisce il seguente messaggio:

converti_angolo.c: In function ‘main’:
converti_angolo.c:39:29: warning: passing argument 2 of ‘converti_angolo’ makes pointer from integer without a cast [-Wint-conversion]
   39 |     converti_angolo(angolo, gradi, primi, secondi);
      |                             ^~~~~
      |                             |
      |                             int
converti_angolo.c:9:42: note: expected ‘int *’ but argument is of type ‘int’
    9 | void converti_angolo(double angolo, int *gradi, int *primi, int *secondi) {
      |                                     ~~~~~^~~~~
converti_angolo.c:39:36: warning: passing argument 3 of ‘converti_angolo’ makes pointer from integer without a cast [-Wint-conversion]
   39 |     converti_angolo(angolo, gradi, primi, secondi);
      |                                    ^~~~~
      |                                    |
      |                                    int
converti_angolo.c:9:54: note: expected ‘int *’ but argument is of type ‘int’
    9 | void converti_angolo(double angolo, int *gradi, int *primi, int *secondi) {
      |                                                 ~~~~~^~~~~
converti_angolo.c:39:43: warning: passing argument 4 of ‘converti_angolo’ makes pointer from integer without a cast [-Wint-conversion]
   39 |     converti_angolo(angolo, gradi, primi, secondi);
      |                                           ^~~~~~~
      |                                           |
      |                                           int
converti_angolo.c:9:66: note: expected ‘int *’ but argument is of type ‘int’
    9 | void converti_angolo(double angolo, int *gradi, int *primi, int *secondi) {
      |  

Il compilatore compila comunque il nostro programma, ma fornisce comunque dei messaggi di avvertimento. Per cui è sempre necessario prestare attenzione a questo tipo di messaggi.

Esempio: ricerca del massimo elemento e del minimo elemento di un array

Guardiamo, adesso, un esempio pratico di utilizzo dei puntatori come argomento di funzioni. Realizziamo una funzione che determina il massimo e il minimo elemento di un array di interi.

Tale funzione, che chiameremo minimo_massimo, prende in ingresso:

  • Un array di interi: int a[];
  • Il numero di elementi dell'array int n;
  • Un puntatore ad intero: int *min;
  • Un puntatore ad intero: int *max.

La funzione trova il massimo ed il minimo e memorizza i due risultati nelle due locazioni di memoria puntate da max e da min.

Una possibile implementazione di questa funzione può essere la seguente:

void minimo_massimo(int a[], int n, int *min, int *max) {
    int i;

    /* Inizializza il minimo con il primo elemento dell'array */
    *min = a[0];

    /* Inizializza il massimo con il primo elemento dell'array */
    *max = a[0];

    /* Parte dal secondo elemento dell'array */
    for (i = 1; i < n; ++i) {
        if (a[i] < *min) {
            *min = a[i];
        }
        if (a[i] > *max) {
            *max = a[i];
        }
    }
}

Proviamo a scrivere un programma completo che legge 10 numeri da tastiera e trova il minimo e il massimo:

#include <stdio.h>

void minimo_massimo(int a[], int n, int *min, int *max);

int main() {
    const int n = 10;
    int a[n];
    int i;
    int min, max;

    printf("Inserisci 10 numeri:\n");

    for (i = 0; i < n; ++i) {
        scanf("%d\n", &a[i]);
    }

    minimo_massimo(a, n, &min, &max);

    printf("Minimo elemento:  %d\n", min);
    printf("Massimo elemento: %d\n", max);

    return 0;
}

void minimo_massimo(int a[], int n, int *min, int *max) {
    int i;

    /* Inizializza il minimo con il primo elemento dell'array */
    *min = a[0];

    /* Inizializza il massimo con il primo elemento dell'array */
    *max = a[0];

    /* Parte dal secondo elemento dell'array */
    for (i = 1; i < n; ++i) {
        if (a[i] < *min) {
            *min = a[i];
        }
        if (a[i] > *max) {
            *max = a[i];
        }
    }
}

Eseguendo il programma otteniamo il seguente risultato:

Inserisci 10 numeri:
56
82
-12
92
124
31
73
47
56
-34
18
Minimo elemento:  -34
Massimo elemento: 124

Esercizi

Di seguito sono riportati degli esercizi per consolidare i concetti appena visti:

In Sintesi

In questa lezione abbiamo visto come passare un puntatore come argomento di una funzione. Questo ci permette di passare degli argomenti per riferimento.

In tal modo, una funzione può modificare il valore di una variabile che è definita all'esterno della funzione.

Nella prossima lezione vedremo come restituire un puntatore come valore di ritorno di una funzione.