Dichiarazioni di Funzioni in Linguaggio C

Nella lezione sulla definizione di funzioni personalizzate in linguaggio c, abbiamo visto che il compilatore C è un compilatore a passo singolo.

In altre parole, quando il compilatore incontra una chiamata a funzione, deve conoscere la definizione della funzione prima di poterla invocare.

In questa lezione vedremo come affrontare questo problema attraverso la dichiarazione di funzioni.

Il problema delle funzioni implicite

Nelle lezioni precedenti, quando abbiamo definito una o più funzioni, abbiamo sempre scritto il codice della funzione prima di utilizzarla.

Ad esempio, volendo scrivere un programma che effettua la somma di due numeri, abbiamo scritto il codice della funzione somma prima di invocarla:

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

int somma(int x, int y) {
    return x + y;
}

int main() {
    int a = 10;
    int b = 20;
    int risultato = somma(a, b);

    printf("La somma di %d e %d è %d\n", a, b, risultato);

    return 0;
}

In questo esempio, la funzione somma è definita alle righe 3-5 e invocata alla riga 10.

Qualsiasi compilatore C legge il codice sorgente dall'inizio alla fine senza mai tornare alle righe precedenti. In gergo tecnico si dice che un compilatore C è un compilatore a passo singolo.

Ciò significa che, esaminando il programma dell'esempio, il compilatore incontra prima la definizione di somma e, successivamente, la sua invocazione. Quando arriva alla sua invocazione, riga 10, il compilatore conosce esattamente come è fatta la funzione nel senso che conosce qual è il tipo di dato che restituisce e di quali e quanti parametri essa ha bisogno.

Tuttavia, definire una funzione prima della sua invocazione non è obbligatorio. Secondo lo standard del linguaggio C, non è necessario definire una funzione prima di invocarla. Avremmo potuto, infatti, scrivere il codice dell'esempio in questo modo:

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

int main() {
    int a = 10;
    int b = 20;
    int risultato = somma(a, b);

    printf("La somma di %d e %d è %d\n", a, b, risultato);

    return 0;
}

int somma(int x, int y) {
    return x + y;
}

In tal caso il compilatore, esaminando il codice dall'inizio, raggiunge la riga 6 e trova la chiamata alla funzione somma. A questo punto il compilatore non conosce nulla della funzione somma. Non sa che tipo di risultato restituisce, ammesso che ne restituisca uno, e non sa quanti parametri e quali tipi di parametri essa accetta.

I compilatori moderni, ossia quelli che adottano lo standard dal C99 in poi, come le ultime versioni di gcc, clang e Microsoft Visual Studio, restituiscono un'eccezione in questo caso. Ad esempio, gcc restituisce il seguente errore:

error: implicit declaration of function 'somma' [-Wimplicit-function-declaration]

Le versioni più vecchie dei compilatori, aderenti allo standard C89, invece, non restituivano eccezioni. Il motivo è che, quando incontravano una chiamata a funzione di cui non conosce la definizione, i compilatori facevano due assunzioni:

  1. Che il tipo restituito sia di tipo int;
  2. Che gli argomenti passati siano del tipo corretto.

Si parla in tal caso di funzioni implicite.

Definizione

Funzioni implicite o Dichiarazione implicita di funzioni

Una Dichiarazione implicita di funzione rappresenta, in linguaggio C, l'invocazione ad una funzione di cui il compilatore non conosce ancora la lista ed il tipo di parametri accettati ne il tipo di valore restituito.

In presenza di una dichiarazione implicita di funzione:

  • I compilatori moderni restituiscono un'eccezione;
  • I compilatori più vecchi assumono che la funzione restituisca un valore di tipo int e che accetti un numero di parametri corrispondente a quelli passati.

A partire dallo standard C99, le dichiarazioni implicite di funzioni sono considerate un errore di compilazione.

Un primo modo di risolvere il problema delle funzioni implicite è quello di adottare lo stile adoperato sinora: definire le funzioni prima del punto in cui vengono effettivamente adoperate.

Questo approccio può risultare semplice per programmi di dimensione ridotta. Purtroppo, al crescere della complessità e, come vedremo, quando si scrivono programmi suddividendoli in più di un file sorgente, questo approccio diventa problematico.

Pertanto, il linguaggio C mette a disposizione un meccanismo per risolvere il problema: le dichiarazioni di funzioni.

Dichiarazioni di funzioni

In un programma C, per aiutare il compilatore a riconoscere le funzioni che verranno definite in seguito, è possibile dichiarare le funzioni prima di definirle.

In parole povere, è come se dessimo un'anteprima al compilatore della funzione che definiremo in seguito.

Una dichiarazione di una funzione rassomiglia alla prima riga della definizione di una funzione, ma senza il corpo della funzione stessa:

tipo_di_ritorno nome_funzione(tipo_parametro1, tipo_parametro2, ..., tipo_parametroN);

Risulta ovvio che, una dichiarazione di funzione deve essere consistente con la successiva definizione della funzione. In particolare, i tipi di ritorno e i tipi dei parametri devono essere gli stessi.

Ritornando all'esempio di sopra, possiamo definire la funzione somma dopo il corpo del main sfruttando una dichiarazione di funzione:

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

/* Dichiarazione di somma */
int somma(int x, int y);

int main() {
    int a = 10;
    int b = 20;
    int risultato = somma(a, b);

    printf("La somma di %d e %d è %d\n", a, b, risultato);

    return 0;
}

/* Definizione di somma */
int somma(int x, int y) {
    return x + y;
}

In questo modo, il compilatore incontra dapprima alla riga 4 la dichiarazione di somma. La dichiarazione rivela al compilatore che esiste da qualche parte una funzione somma che restituisce un valore di tipo int e accetta due parametri di tipo int.

Successivamente, alla riga 9, il compilatore trova un'invocazione alla funzione somma. Conoscendone la dichiarazione, il compilatore può controllare che gli argomenti passati siano effettivamente di tipo int (come nell'esempio) e che il tipo di ritorno sia compatibile con il tipo di variabile a cui assegnamo il risultato.

Infine, alla riga 17, il compilatore trova la definizione di somma. In questo caso, il compilatore può controllare che la definizione sia consistente con la dichiarazione.

Le dichiarazioni di funzioni che abbiamo descritto prendono anche il nome, molto usato, di prototipi di funzione.

Poiché ciò che interessa al compilatore sono il numero e il tipo dei parametri, più il tipo di ritorno, possiamo dichiarare il prototipo di una funzione senza specificare i nomi dei parametri:

int somma(int, int);
Definizione

Prototipo di funzione

Una Dichiarazione di Funzione o Prototipo di Funzione rappresenta un'anteprima della definizione di una funzione. Essa specifica il tipo di ritorno della funzione e il tipo dei parametri che accetta.

La sintassi da utilizzare per dichiarare una funzione è la seguente:

tipo_di_ritorno nome_funzione(tipo1 parametro1, ..., tipoN parametroN);

Il nome dei parametri può essere omesso, specificando solo il tipo dei parametri:

tipo_di_ritorno nome_funzione(tipo1, ..., tipoN);

Le dichiarazioni di funzioni sono particolarmente utili quando si scrivono programmi di dimensioni maggiori, in cui le funzioni sono definite in file sorgente separati come vedremo nelle lezioni future.

Consiglio

Inserire sempre il nome dei parametri nei prototipi di funzione

Sebbene non sia obbligatorio, il consiglio è quello di specificare sempre il nome dei parametri nei prototipi di funzione. Questo rende il codice più leggibile e chiaro.

Inserendo anche i nomi, infatti, si documenta lo scopo di ognuno e si ricorda agli sviluppatori che usano la funzione l'ordine in cui gli argomenti devono essere passati.

Come abbiamo visto sopra, a partire dallo standard C99 le dichiarazioni implicite di funzioni sono considerate un errore di compilazione. Pertanto, l'utilizzo dei prototipi di funzioni è quasi sempre necessario.

Firma di una funzione

Un concetto molto importante, legato al prototipo di una funzione, è quello di firma di una funzione o signature.

In sostanza, la firma di una funzione è costituita dal nome della funzione più la lista dei tipi dei parametri.

Prendiamo, ad esempio, la funzione che segue:

int somma(int x, int y);

La firma di questa funzione è somma(int, int). Da notare che la firma non include il tipo di ritorno.

Il concetto di firma è molto importante in linguaggio C, in quanto, esistono due limitazioni fondamentali:

  • Non è possibile definire due funzioni con la stessa firma. In altre parole, non è possibile definire due funzioni con lo stesso nome e la stessa lista di tipi di parametri ma che restituiscono tipi di ritorno diversi.

    Ad esempio, è un errore definire due funzioni così:

    int somma(int x, int y);
    float somma(int x, int y);
    

    Questo perché entrambe le funzioni hanno la stessa firma somma(int, int) e il compilatore non sa quale delle due chiamare.

  • Non è possibile definire due funzioni con lo stesso nome. Anche se le due funzioni hanno firme diverse, non è possibile definire due funzioni con lo stesso nome.

    Ad esempio, è un errore definire due funzioni così:

    int somma(int x, int y);
    int somma(float x, float y);
    

    Anche in questo caso, il compilatore non sa quale delle due chiamare.

Il linguaggio C++, che può essere considerato a tutti gli effetti un'estensione del linguaggio C, rimuove la seconda limitazione come vedremo nelle lezioni sul C++. In C++, infatti, è possibile definire due funzioni con lo stesso nome ma firme diverse.

Per il linguaggio C, invece, vale la regola che non è possibile definire due funzioni con lo stesso nome.

Definizione

Firma di una funzione

La firma di una funzione, chiamata anche signature di una funzione, è costituita dal nome della funzione più la lista dei tipi dei parametri.

La firma di una funzione non include il tipo di ritorno.

Data una funzione:

tipo_ritorno nome_funzione(tipo1 parametro1, ..., tipoN parametroN);

La firma di questa funzione è nome_funzione(tipo1, ..., tipoN).

In linguaggio C, non è possibile definire due funzioni con la stessa firma.

Inoltre, non è possibile definire due funzioni con lo stesso nome.

In Sintesi

In questa lezione abbiamo introdotto un particolare concetto del linguaggio C: le dichiarazioni di funzioni o prototipi di funzione.

Le dichiarazioni di funzioni sono utili per risolvere il problema delle funzioni implicite. Queste ultime rappresentano chiamate a funzioni di cui il compilatore non conosce la definizione.

Il compilatore C è un compilatore a passo singolo. Ciò significa che, quando incontra una chiamata a funzione, deve conoscere la definizione della funzione prima di poterla invocare. Nello standard C99 e successivi, le dichiarazioni implicite di funzioni sono considerate un errore di compilazione.

Le dichiarazioni di funzioni permettono di dichiarare una funzione prima di definirla. Una dichiarazione di funzione è costituita dal tipo di ritorno della funzione, dal nome della funzione e dalla lista dei tipi dei parametri.

Abbiamo, inoltre, introdotto il concetto di firma di una funzione. La firma di una funzione è costituita dal nome della funzione più la lista dei tipi dei parametri. La firma di una funzione non include il tipo di ritorno.

Nella prossima lezione approfondiremo il concetto di argomento e parametro di una funzione.