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 |
|
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 |
|
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:
- Che il tipo restituito sia di tipo
int
; - Che gli argomenti passati siano del tipo corretto.
Si parla in tal caso di funzioni implicite.
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 |
|
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);
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.
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.
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.