Le funzioni

25.1 Le funzioni

Le funzioni sono uno strumento del linguaggio C utile per scrivere dei piccoli blocchi di istruzioni richiamabili in vari punti del programma e che possono ritornare dei valori; un po' come le procedure del Pascal, le perform del Cobol o le subroutines del Fortran o del Basic (le gosub). Una funzione puo' essere contenuta all'interno di una libreria (ad esempio le funzioni delle librerie standard) oppure essere sviluppata all'interno del programma. Quando si utilizza una funzione di libreria come ad esempio printf() o getchar(), non e' necessario sapere come e' stata implementata, occorre solo sapere quali parametri accetta in input e quali risultati ritorna in output. Ma e' possibile creare delle funzioni proprie per rendere piu' leggibile il programma (cioe' strutturato). Quando si crea una funzione propria occorre prima dichiararla e poi definirla. La dichiarazione di una funzione e' simile alla dichiarazione di una variabile ed ha il formato seguente:

tipo-ritornato	nome-funzione 	(lista parametri opzionale)

ad esempio: 'int miafunz (int c)'. Tale dichiarazione prende il nome di prototipo della funzione ed e' obbligatoria. Nella definizione di una funzione invece vengono definite le istruzioni in essa contenute. Una definizione di funzione ha il formato seguente:

tipo-ritornato	nome-funzione 	(lista parametri opzionale)
{
dichiarazioni
istruzioni
}

Le definizioni possono comparire in qualsiasi ordine all'interno del programma, ma non possono essere spezzate su file separati. Nel C, a differenza di quanto accade in altri linguaggi, non e' possibile definire delle funzioni all'interno di altre funzioni. E' possibile pero' scrivere delle funzioni ricorsive, cioe' delle funzioni che chiamano se stesse. Poiche' il C non contiene un operatore esponenziale per l'operazione di elevamento a potenza (esiste la funzione di libreria standard pow()) si potrebbe definire una funzione che effettua tale operazione:

#include <stdio.h>

int power (int m, int n);

/* il programma usa la funzione power */
main()
{
	int i;

	for (i=0; i < 10;   i)
	    printf ("%d %d %d\n", i, power (2,i), power (-3,i));

	return 0;
}

/* power: eleva base all'n-esima potenza; n >= 0 */
int power (int base, int n)
{
	int i, p;
	p = 1;
	for (i= 1; i <= n;   i)
	    p = p * base;
	return p;
}

quando viene valutata un'espressione, viene calcolato un valore numerico che in caso di assegnamenti e' il valore dell'operando di sinistra. Analogamente se all'interno di un'espressione e' contenuta una funzione, il valore numerico di tale espressione corrisponde al valore ritornato dalla funzione. Percio' nel programma illustrato sopra l'espressione power (2,i) corrisponde al valore ritornato da tale funzione. Si tratta di un intero (cosi' come dichiarato nel prototipo) e come tale viene trattato dalla funzione printf(). Quando la variabile 'i' contiene il valore 3 ad esempio, alla funzione power() vengono passati i valori 2 e 3: power(2, 3). Il risultato ritornato dalla funzione power() in questo caso sara' 9 e nella funzione printf l'espressione power (2, 3) sara' sostituita da 9. Il parametro '%d' della funzione printf() infatti tratta tale valore come un intero. La seconda riga del programma e' il prototipo della funzione, ossia la dichiarazione della funzione. Poiche' nel prototipo sono contenuti i tipi dei parametri e del valore ritornato, il compilatore puo' effetturare un controllo di consistenza tra tali tipi e quelli effettivamente usati nella definizione e nella chiamata della funzione. Cio' e' utile in quanto vengono segnalati dal compilatore eventuali errori in caso di uso incongruente delle variabili. I nomi delle variabili dichiarate nella definizione della funzione power() sono locali alla funzione, nel senso che sono visibili solo dalla funzione. Ad esempio la variabile 'i' dichiarata all'interno della funzione main() e la variabile 'i' dichiarata all'interno della funzione power() sono due variabili diverse ed occupano spazi diversi in memoria. Le variabili usate all'interno delle parentesi tonde nella definizione di una funzione sono dette parametri mentre le variabili usate all'interno delle parentesi tonde nella chiamata alla funzione sono dette argomenti. Il valore calcolato dalla funzione power() viene ritornato alla funzione main() mediante l'istruzione 'return'. L'istruzione 'return espressione', ritorna il valore prodotto dall'espressione. Se necessario tale valore viene convertito nel tipo ritornato dalla funzione (cosi' come specificato nel prototipo e nella definizione) prima che venga eseguita l'istruzione return. L'espressione che segue la parola chiave return puo' essere racchiusa per chiarezza da parentesi tonde che sono tuttavia opzionali. Se per un qualsiasi motivo la funzione termina senza eseguire l'istruzione return (perche' viene raggiunta la parentesi graffa di chiusura), al chiamante non viene restituito alcun valore. Nei casi in cui una funzione non riesce a restituire un valore, cio' che viene ricevuto dal chiamante e' imprevedibile. Occorre osservare che anche la funzione main() contiene una istruzione return. Infatti la funzione main() e' una funzione come tutte le altre e puo' ritornare (anzi dovrebbe) un valore al programma che l'ha invocata. Il programma che ha chiamato la funzione main() e' l'ambiente che ha chiamato il programma (ad esempio il sistema operativo Linux). Per convenzione viene ritornato il valore 0 se l'operazione e' andata a buon fine, in caso contrario viene ritornato un valore diverso da zero. Quando non viene specificato il tipo ritornato da una funzione per il compilatore assume che il valore ritornato sia di tipo int. Cosi' accade anche per il main in quanto all'inizio del programma sarebbe stato piu' corretto scrivere: 'int main ()'. Infatti occorre sempre specificare il tipo ritornato da una funzione che dovrebbe essere void (nessun tipo) se non viene ritornato nulla, oppure int, char, long etc se viene ritornato un valore. La funzione main() non necessita di un prototipo e puo' assumere i seguenti formati:

int main (void) {istruzioni}

   oppure

int main (int argc, char *argv[]) {istruzioni}

Il tipo int ritornato, se omesso viene assunto per default dal compilatore che tuttavia potrebbe produrre un messaggio di avvertimento (warning) del tipo: 'function should return a value' in caso di assenza dell'istruzione return. Da questo momento in poi i listati forniti conterranno sempre l'istruzione return all'interno della funzione main(). I parametri argc ed argv sono rispettivamente un intero ed un vettore di puntatori a carattere. Il primo fornisce il numero di argomenti passati alla funzione main() cioe' al programma, il secondo fornisce un vettore di puntatori agli argomenti passati. Questi argomenti saranno chiari in seguito quando verranno illustrati i puntatori. Nel prototipo della funzione power() vengono specificati due interi come parametri, stessa cosa vale per la definizione di tale funzione e analogamente quando viene invocata. Qualora il compilatore riscontrasse delle incongruenze tra i tipi specificati nel prototipo e quelli usati nella definizione o all'atto dell'invocazione della funzione, produrrebbe un errore di avvertimento del tipo: 'type mismatch in ...' evidenziando il numero di riga che contiene l'errore. I messaggi di errore qui illustrati sono ipotetici e quindi sono indicativi, in quanto variano da compilatore a compilatore: non e' importante l'esatta dicitura ma e' importante capire il concetto che sta dietro tale dicitura. Il senso del messaggio dovrebbe essere piu' o meno: 'non c'e' corrispondenza di tipo' oppure 'il tipo usato in questo punto non e' lo stesso tipo usato in quell'altro punto' oppure 'esiste una incongruenza tra i tipi usati' e via dicendo. In definitiva e' necessaria una perfetta corrispondenza di tipo tra quando specificato nel prototipo della funzione (dichiarazione), tra quanto specificato nella definizione della funzione (dove vengono inserite le istruzioni) e tra quanto specificato all'atto dell'invocazione della funzione. Qualsiasi discrepanza verra' considerata un errore. Per quanto riguarda i nomi usati al contrario, non e' necessaria una corrispondenza: all'interno del prototipo i nomi sono addirittura opzionali (solo la specificazione del tipo e' necessario). Il prototipo della funzione power() si sarebbe potuto scrivere cosi': int power (int, int). Ad ogni modo, per motivi di leggibilita' e' consigliato un uso coerente dei nomi. Gli argomenti delle funzioni in C vengono passati per valore a differenza di quanto avviene per altri linguaggi dove esiste anche la chiamata per riferimento. Quando un argomento viene passato per valore, cio' che viene passato alla funzione non e' l'argomento vero e proprio ma il suo valore. Se ad una funzione viene passata ad esempio una variabile 'pippo' di tipo int, cio' che viene passato non e' l'indirizzo della locazione di memoria che corrisponde alla variabile 'pippo' ma semplicemente il suo valore. Se la variabile 'pippo' contiene il valore '3' alla funzione viene passato '3'. Poiche' la funzione deve gestire tale valore, ha la necessita' di memorizzarlo da qualche parte: deve cioe' possedere una variabile locale che puo' contenere tale valore. I parametri usati nella definizione della funzione sono le variabili locali che conterranno gli argomenti passati all'atto dell'invocazione. Nel programma illustrato sopra, 'base' e 'n' sono le variabili locali che conterranno i valori passati al momento dell'invocazione. Infatti quando la funzione power() viene chiamata, gli argomenti passati verranno copiati all'interno delle variabili 'base' e 'n'. La conseguenza di cio' e' che una funzione non puo' alterare direttamente una variabile di proprieta' della funzione chiamante, ma puo' variare unicamente la sua copia privata e temporanea. Si dice cioe' che la variabile usata nella funzione chiamante non e' visibile dalla funzione chiamata e viceversa. L'ambito di visibilita' di una variabile viene definito scope che significa appunto dominio, ambito, portata, campo d'azione. Il passaggio di argomenti per valore e' un vantaggio in quanto la funzione chiamata puo' usare liberamente le sue variabili locali senza che cio' sia fonte di conflitti con le variabili usate dalla funzione chiamante. Anzi, la funzione chiamante potrebbe ignorare completamente l'implementazione di una funzione chiamata (basti pensare alle funzioni di libreria standard) e potrebbe casualmente usare gli stessi nomi usati all'interno di tale funzione. Questo meccanismo di occultamento e' cio' che viene definito incapsulamento nei linguaggi ad oggetti. Tuttavia in alcune circostanze puo' essere necessario che una funzione chiamata sia in grado di modificare le variabili della funzione chiamante. Per rendere visibili le variabili che sono fuori dalla portata della funzione chiamata, si possono usare i puntatori, ma cio' sara' illustrato in seguito. Tutto quanto detto fino a questo punto e' valido per tutti i tipi di argomenti passati, tranne che per i vettori. I vettori sono un tipo di dato derivato ed hanno una particolarita': il nome del vettore corrisponde all'indirizzo del primo elemento del vettore stesso. Quando viene passata una variabile di qualsiasi tipo come argomento di una funzione, cio' che viene passato e' una copia del valore contenuto in tale variabile. Cio' non e' vero se come argomento viene passato il nome di un vettore. In questo caso infatti cio' che viene passato e' l'indirizzo del primo elemento del vettore. Incrementando tale indirizzo la funzione puo' accedere a qualsiasi elemento contenuto all'interno del vettore. Qualsiasi variabile, prima di poter essere usata, deve essere dichiarata. La posizione nella quale viene dichiarata determina la visibilita' di tale variabile. Una dichiarazione specifica un tipo e contiene una o piu' variabili separate da una virgola, ad esempio:

int a, b, c;

oppure

int a;
int b;
int c;

oppure

char c, vetto[10];

il nome di una variabile per convenzione e' scritto in minuscolo per distinguerla da una costante (cio' non toglie che si possono usare nomi in maiuscolo se si vuole). Se una dichiarazione contiene un tipo ed una sola variabile (es: int a;) tale variabile puo' essere inizializzata all'interno della dichiarazione stessa (es: int a = 0;). I nomi esterni possono essere lunghi fino a 31 caratteri mentre i nomi interni fino a 63. Questo quanto imposto dallo standard che tra l'altro consiglia alle implementazioni di non imporre limitazioni di questo tipo. Infatti su alcuni compilatori le variabili con nomi piu' lunghi di 63 caratteri vengono trattate senza produrre errori. I nomi delle variabili devono essere diversi dai nomi usati dal C (le cosidette parole chiave) che attualmente sono circa una quarantina (32 nella prima versione del C). Le parole chiave del C devono essere scritte in minuscolo e sono ad esempio: if, else, int, long, void, while, for e via dicendo. Una variabile viene inizializzata ogni volta che si entra all'interno della funzione dove e' dichiarata: poiche' la funzione main() corrisponde al programma, le variabili dichiarate nella funzione main() vengono inizializzate una volta soltanto quando parte il programma. In altre parole una variabile dichiarata all'interno di main() nasce quando parte il programma e muore quando il programma termina. Stessa cosa vale per le variabili dichiarate all'interno delle altre funzioni: nascono quando viene invocata la funzione e muoiono quando tale funzione termina. Nel programma precedente ad esempio, la variabile 'p' nasce quando viene invocata la funzione power() ma muore quando termina tale funzione (cioe' viene eseguita l'istruzione 'return p'). In altre parole le variabili sono private e locali all'interno di una funzione o, piu' esattamente all'interno delle parentesi graffe che delimitano il corpo della funzione. Le variabili locali di una funzione sono dette anche variabili automatiche. Poiche' una variabile nasce quando viene invocata la funzione che la dichiara e muore quando termina tale funzione, ne consegue che il valore contenuto all'interno di tale variabile tra una chiamata e l'alta e' indefinito. Tra una chiamata e l'altra la variabile locale alla funzione puo' contenere qualsiasi valore. Per questo motivo tali variabili devono essere inizializzate ad ogni chiamata della funzione. Esiste pero' la possibilita' di specificare delle variabili esterne a qualsiasi funzione ossia visibili da qualsiasi funzione. Tali variabili non sono temporanee come le comuni variabili e continuano a vivere tra una chiamata e l'altra delle varie funzioni. Tali variabili vengono definite esterne e devono essere definite una volta sola all'esterno di qualsiasi funzione. Qualsiasi funzione che usa tali variabili deve pero' dichiararle prima di poterle usare. Per esplicitare la natura globale di queste variabili puo' essere usata la parola chiave extern all'interno della dichiarazione (ad esempio 'extern int a'). La parola chiave extern e' implicita se la definizione della variabile precede la funzione. Se al contrario la variabile e' stata definita in un file diverso da quello che contiene il programma, la dichiarazione extern diventa obbligatoria. Con il termine di definizione di una variabile si intende il punto dove la variabile viene creata e viene allocato lo spazio necessario in memoria, mentre con il termine di dichiarazione si intende il punto dove viene dichiarato unicamente il tipo di variabile ma non viene allocato spazio in memoria. Infatti una variabile locale all'interno di una funzione viene dichiarata e creata quando viene invocata la funzione stessa. In questo caso viene allocato nuovo spazio in memoria ad ogni chiamata della funzione. Quando pero' all'interno di una dichiarazione e' presente la parola chiave extern, la variabile non e' piu' allocata ad ogni chiamata della funzione, perche' la variabile e' esterna alla funzione. Solo se una variabile non e' esterna, la definizione e la dichiarazione coincidono. Una variabile esterna viene inizializzata solamente al momento della sua definizione (e non quando viene dichiarata). Una variabile esterna e' visibile all'esterno del file dove viene definita, utilizzando la parola chiave extern. Tuttavia e' possibile limitare la visibilita' di una varibile esterna utilizzando la parola chiave static (ad esempio 'static int a'). Cio' puo' essere utile se le funzioni che compongono un programma vengono scritte su file diversi e le variabili usate devono essere viste solo dalle funzioni e non da chi le chiama. E' un po' come il concetto di incapsulamento che si ha nella programmazione ad oggetti. Una variabile automatica (cioe' creata all'interno di una funzione) normalmente viene inizializzata ad ogni chiamata della funzione. Tuttavia la parola chiave static, se abbinata ad una variabile automatica, permette di mantenere lo stesso valore della variabile stessa tra una chiamata e l'altra della funzione. Anche le funzioni possono essere dichiarate static. Una funzione dichiarata static e' visibile solo all'interno del file dove e' stata definita. In genere lo scope o ambito di visibilita' di una variabile comincia dal punto dove tale variabile viene dichiarata fino alla fine del file. Ad eccezione delle variabili dichiarate all'interno di blocchi logici (parentesi graffe): in questo caso infatti la visibilita' termina alla chiusura della parentesi graffa. Percio' nel codice seguente:

int a;

if (a > 0)
   {
    int b = 0;
    b  ;
   }
else
   {
    int b = 1;
    printf ("a maggiore di zero");
   }

printf("%d", b);

la variabile b contenuta all'interno del ramo 'then' della if (in caso di condizione verificata) e' una variabile completamente diversa dalla variabile b contenuta nel ramo 'else' (in caso di condizione non verificata). Inoltre l'ultima printf() produrra' un errore di compilazione del tipo 'undefined symbol', in quanto la variabile b non e' definita (o meglio non visibile al di fuori del costrutto if). Una variabile puo' essere definita di tipo register (ad esempio 'register int a) se deve essere utilizzata frequentemente. Le variabili register dovrebbero essere collocate all'interno dei registri della macchina, ma il compilatore puo' ignorare questa direttiva. Solo le variabili automatiche (cioe' definite all'interno di una funzione) ed i parametri delle funzioni possono essere dichiarati di tipo register. Indipendentemente dal fatto che le variabili register vengano allocate nei registri o meno, di loro, a differenza delle altre variabili, non e' possibile conoscerne l'indirizzo.

25.2 Inizializzazione delle variabili

Le variabili esterne e quelle statiche sono inizializzate a zero. Le variabili automatiche e register al contrario, devono essere inizializzate esplicitamente, altrimenti il loro contenuto risulta imprevedibile. Le variabili vengono inizializzate esplicitamente usando delle costanti oppure con delle espressioni. I vettori vengono inizializzati con delle stringhe costanti o mediante una lista di valori iniziali tra parentesi graffe. Quando il numero degli inizializzatori contenuti all'interno della lista e' inferiore al numero degli elementi del vettore, i restanti elementi vengono inizializzati automaticamente a zero se si tratta di un vettore esterno o static (ma se il vettore e' una variabile automatica il loro contenuto e' indefinito). Esempi:

int a = 0;
int a = b - 1;
int a = b = 0;
char stringa[] = "ciao";                             /* vettore e' lungo 5 */
char stringa[] = {'c', 'i', 'a', 'o', '\0'};         /* lo stesso vettore */
int vetto[] = {1, 2, 3, 4, 5};
int vetto[5] = {1, 2, 3}                             /* gli elementi vetto[3] e vetto[4] vengono inizializzati a zero */

Inizio della guida  I vettori  Indice  Gli operatori

Copyright (c) 2002-2003 Maurizio Silvestri