Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

domenica 20 gennaio 2013

Prototipi? Si grazie!
come usare i Prototipi in C

Dopo i bagordi delle feste di fine anno (e, magari, qualche chiletto accumulato da smaltire), meglio cominciare con un argomento leggero leggero: i Prototipi di Funzione. Leggero, ma non troppo.

Dopo una rapida ispezione in rete ho notato una certa confusione sull'argomento. Prototipi obbligatori, forse consigliati, a volte sconosciuti... ho notato informazioni fuorvianti perfino in dispense universitarie (ahi, ahi). Tra l'altro, nei miei trascorsi, ho parlato con vari colleghi C-Programmers che non avevano le idee chiare sull'argomento. Beh, allora è giunta l'ora di fare chiarezza!

Partiamo dai dati di fatto, lasciando alla seconda parte del post le considerazioni tecniche/filosofiche sull'argomento. Mi raccomando di prestare attenzione, nel seguito del testo, ad alcune parole chiave che useremo e cercheremo di illustrare: prototipo, dichiarazione e definizione. E, faremo riferimento anche alle varie versioni del C che ci hanno accompagnato fino ad oggi, che in ordine di tempo sono: K&R C, ANSI C (C89/C90) e C99 (ci sarebbe anche il C11, ma non è significativo per questo post). Se non altrimenti specificato tutte le prossime affermazioni/considerazioni si riferiranno al C attuale, il C99.

Veniamo al dunque: nel C i prototipi non sono obbligatori. La confusione su quest'argomento deriva dalla doppia personalità che hanno molti programmatori C (incluso il sottoscritto) che devono, spesso, districarsi tra C e C++ facendo, a volte, un po' di confusione: i prototipi sono obbligatori nel C++, per motivi strettamente collegati ad alcune funzionalità del linguaggio (vi suona il Function Overloading?).

Nel C, invece, è obbligatoria la dichiarazione di una funzione.

Facciamo, allora, un esempio sulle parole chiave dichiarazione, prototipo e definizione, usando solo una sintassi di tipo moderno (ANSI C o C99):
// dichiarazione di funzione
int myFunc();

// dichiarazione di funzione con prototipo
int myFunc(int val):

// definizione di funzione con prototipo
int myFunc(int val)
{
   if (val > 5)
       return val;
   else
       return val * 2;
}
L'ordine nell'esempio descritto, come evidente, non è casuale: la dichiarazione è il caso basico; il prototipo contiene implicitamente una dichiarazione, e, infine, la definizione contiene implicitamente un prototipo (e quindi anche una dichiarazione). Come detto, nell'esempio ho omesso, per non complicare inutilmente la descrizione, le sintassi permesse ma troppo old-fashioned, o le sintassi vietate dal C99.

Prima di passare alla parte filosofica, facciamo una breve analisi storica: nel K&R C non c'era l'obbligo di dichiarazione delle funzioni, quindi non c'era nessun controllo a compile-time sul valore di ritorno e, ancor meno, sulla coerenza dei parametri passati: in mancanza della dichiarazione il compilatore applicava un comportamento di default e assumeva che la funzione ritornava un int. Per i parametri si applicava la default argument promotion: gli interi venivano promossi a int, e i float erano promossi a double.

Con l'avvento del ANSI C (o C89/C90), sono arrivati i prototipi, però è stata mantenuta la retrocompatibilità con la vecchia sintassi (per non obbligare a sistemare milioni di linee di codice funzionante). Con questa novità era, finalmente, possibile controllare a compile-time la correttezza d'uso delle funzioni, sia sui parametri che sui valori di ritorno. A causa della retrocompatibilità rimaneva, però, possibile scrivere nuovo codice con la sintassi antica, e, inoltre, rimaneva valido il concetto del default return value in assenza di dichiarazione.

Con il C99 si è fatto un ulteriore passo in avanti: va bene la ricerca della compatibilità con il codice pre-esistente, ma il valore di ritorno di default era una falla troppo grande nella solidità del linguaggio, per cui si è introdotta la dichiarazione obbligatoria, come indicato all'inizio del post (aggiungo che si è anche reso obbligatorio l'uso dei prototipi negli standard headers del linguaggio, ma questa è un altra storia...).

E ora, dopo avere descritto quello che lo standard ci obbliga e/o permette di fare, veniamo, finalmente, a ciò che è meglio fare: secondo me un buon programmatore usa i prototipi (quindi, presumo, per la proprietà transitiva chi non usa i prototipi non è un buon programmatore. Ho detto presumo, quindi se qualcuno si è offeso non se la prenda con me, se la prenda con la proprietà transitiva). E perché consiglio così caldamente l'uso dei prototipi? Beh, il C é un linguaggio tipizzato, per cui è così evidente l'aiuto che questo meccanismo ci può dare per produrre codice senza errori di tipo, migliorando al tempo stesso leggibilità e manutenibilità, che non c'è neanche bisogno di spiegarlo!

E, per aggiungere un tocco di radicalità che non guasta mai, aggiungo che, per le suddette questioni di leggibilità e manutenibilità del software, non è conveniente affidarsi al fatto che usando definizioni con prototipo (vedi esempio sopra), e scrivendo il codice nel giusto ordine (cioè usando una funzione solo dopo la sua definizione), non è necessario scrivere dei veri e propri prototipi. Non siate pigri nelle cose utili, per favore!

E come deve essere strutturato un buon codice rispetto a quanto detto sopra? Vediamo un breve esempio con tre file: un header, un file con funzioni, e un file che le usa:

Questo è l'Header file:
/* myfuncs.h
 */
// prototipi globali
char *myFunc1(char *dest, const char *src);
char *myFunc2(char *dest, const char *src);
Ecco il file con le funzioni:
/* myfuncs.c
 */
#include "myfuncs.h"

// myFunc1()
char *myFunc1(char *dest, const char *src)
{
    ...
}

// myFunc2()
char *myFunc2(char *dest, const char *src)
{
    ...
}
E, infine, il file utilizzatore:
/* usefuncs.c
 */
#include "myfuncs.h"

// prototipi locali
static int useFuncs(void);
static int anotherFunc(void);

// anotherFunc()
static int anotherFunc(void)
{
    ...
    int res = useFuncs();
    ...
}

// useFuncs()
static int useFuncs(void)
{
    ...
    char *p1 = myFunc1(dest, src);
    char *p2 = myFunc2(dest, src);
    ...
}
E ho detto tutto!

Ciao e al prossimo post.