Le funzioni. di Roberto Porro Fino a questo momento abbiamo utilizzato semplici programmi costituiti da una sola funzione: la main ed alcune funzioni di libreria, quali la printf, scanf, ecc. Nella stesura di programmi C reali pero' si articola il programma in varie 'funzioni' che hanno il compito di eseguire compiti specifici. La funzione C e' un blocco di codice autocontenuto, ovvero completamente definito, che riceve dall'esterno i parametri in ingresso e ritorna dati in uscita, nonche' gli effetti che essa stessa provoca durante la sua esecuzione. Si puo' immaginarla come una 'scatola nera' che interagisce col mondo esterno (il programma). Come e' stato gia' detto in precedenza l'unica funzione che obbligatoriamente deve essere presente in un programma C e' la main, poi si possono aggiungere tutte le funzioni che necessitano, senza limite di numero. Le funzioni possono richiamare altre funzioni, possono anche richiamarsi (ricorsione), possono modificare i parametri in ingresso, ma devono ritornare il controllo al programma main quando terminano. (Solo in caso di uscita per errore si puo' evitare di ritornare al main tramite, ad esempio, la 'exit'). Non esistono regole per l'utilizzo delle funzioni: non vi e' precedenza, profondita' di annidamento o regole gerarchiche. In pratica l'unica limitazione e' la memoria del sistema e lo stack. Ricordiamo che ogni volta che viene chiamata una funzione il sistema salva sullo stack gli indirizzi del chiamante, alloca spazio per i parametri e tutto cio' occupa memoria. Dopo queste premesse di ordine generale analizziamo l'uso delle funzioni. /* Programma: FUNZIONE */ #includevoid main( void ); int funz1( int ); void main( void ) { int i; for( i = 0; i < 10; i++ ) printf( "Chiamo funz1 volta n. %d\n", funz1( i )); } int funz1( int par1 ) { printf( "col valore %d ", par1 ); return( par1+1 ); } Il programma FUNZIONE utilizza una funzione 'funz1' per stampare una serie di valori. La funz1 viene definita come: int funz1( int ); il che significa che funz1 ritorna un valore di tipo int ed utilizza un parametro di tipo int. Il parametro passato e' l'indice del ciclo for. La printf esegue funz1( i ) che stampa col valore e visualizza par1, quindi ritorna par1+1. Infine visualizza Chiamo funz1 volta n. e stampa il valore di ritorno par1+1, ovvero i+1. In pratica si ottiene un output tipo: col valore 0 Chiamo funz1 volta n. 1 col valore 1 Chiamo funz1 volta n. 2 ecc... funz1 potrebbe richiamare una ipotetica funz2 passando non piu' un tipo intero ma un carattere o una stringa di caratteri, oppure funz2 potrebbe seguire il ciclo for nel main ed eseguire un altro compito e cosi' via. Il numero dei parametri e' limitato solo dalle necessita' del programma e, se non servono parametri in ingresso o valori di ritorno si utilizzi il tipo 'void' per definire la funzione. Ecco alcuni esempi di funzione: void funz( void ); /* nessun valore in ingresso nessun valore di ritorno */ int funz( int, int ); /* due interi in ingresso ritorna un tipo int */ char funz( char* ); /* un array di caratteri in ingresso ritona un tipo char */ int funz( char, int, double, char* ); /* quattro parametri in ingresso: carattere, intero, double e array di caratteri. Ritorna un intero */ E' importante assicurarsi che i parametri in ingresso siano coerenti con la definizione del loro tipo per non incorrere in warning in fase di compilazione o a malfunzionamenti in fase di run-time. - Funzioni ricorsive. In C si usa spesso la ricorsione, ovvero una funzione che richiama se stessa. Es.: main() { printf( "main\n" ); main(); } Questo programma si richiama all'infinito e deve essere interrotta dall'utente con l'appropriata sequenza di stop (CTRL+D sul QL, CTRL+C su PC-DOS, ecc...). - Funzioni, puntatori e valori di ritorno. Vediamo un altro uso delle funzioni e dei parametri. Una funzione ritorna un solo valore, per cui se si devono modificare piu' valori all'interno della funzione e tali valori ci servono nel corpo della funzione chiamante dobbiamo usare un piccolo espediente. Analizziamo il seguente codice. Supponiamo di dover modificare il contenuto di due variabi- li: una stringa di caratteri c ed un intero d. Sia la stringa che l'intero sono noti all'interno della main, ma non sono noti alla negac che li acquisice come parametri in ingresso. La negac e' definita come una funzione che ritorna un puntatore a carattere, quindi non potrebbe ritornare il valore di una variabile di tipo intero, come effettivamente fa. Dal momento che abbiamo deciso di modificare il valore sia della stringa c che dell'intero d all'interno della negac dobbiamo comportarci nel seguente modo: 1)negac ritorna un puntatore a carattere, ovvero l'indirizzo del primo carattere della stringa; 2)negac accetta come parametri un puntatore a carattere ed un puntatore ad intero, ovvero gli indirizzi di una stringa e di un intero. Passando c alla negac in realta' si fornisce il puntatore ad un indirizzo di memoria che e' la locazione in cui e' contenuta la stringa c, cosi' come passando &d si fornisce il puntatore all'indirizzo in cui e' contenuta la variabile di tipo intero d. A questo punto nella funzione negac, non sono contenute le immagini delle variabili c e d, ma i loro veri indirizzi, per cui ogni modifica effettuata a queste variabili altera il contenuto delle locazioni di memoria delle due variabili e, quindi, permette di avere in main i valori modificati delle stesse. N.B. i nomi dei parametri di negac, ovvero f e k, sono simbolici ed assumono l'indirizzo di c e d, per cui sono a tutti gli effetti equivalenti alle variabili dichiarate nella main!! #include char* negac( char*, int* ); main() { char c[4]; int d = 0; sprintf( c, "%s", "SI" ); printf( "Inizio: c vale %s e d %d\n", c, d ); negac( c, &d ); printf( "Ritorno da negac: c vale %s e d %d\n", c, d ); } char* negac( char* f, int *k ) { printf( "Ingresso di negac: c vale %s e d %d\n", f, *k ); sprintf( f, "%s", "NO" ); *k+=1; } Questo e' l'output del programma. Inizio: c vale SI e d 0 Ingresso di negac: c vale SI e d 0 Ritorno da negac: c vale NO e d 1 Se avessimo scritto il programma nel seguente modo: #include char* negac( char*, int ); main() { char c[4]; int d = 0; sprintf( c, "%s", "SI" ); printf( "Inizio: c vale %s e d %d\n", c, d ); negac( c, d ); printf( "Ritorno da negac: c vale %s e d %d\n", c,d ); } char* negac( char* f, int k ) { printf( "Ingresso di negac: c vale %s e d %d\n", f, k ); sprintf( f, "%s", "NO" ); k+=1; } l'output sarebbe stato: Inizio: c vale SI e d 0 Ingresso di negac: c vale SI e d 0 Ritorno da negac: c vale NO e d 0 in quanto d viene passato come parametro, giunti in negac k contiene l'immagine di d ma la locazione di memoria non e' quella di d, k viene incrementato, la funzione ritorna il valore della stringa mentre viene perso il contenuto di k e, nella main, il valore di d resta invariato. Bisogna prestare attenzione a questo tipo di utilizzo delle funzioni e dei loro parametri in quanto e' facile incorrere in errori o blocchi di sistema, poiche' si puo' andare a far riferimento ad aree di memoria inesistenti oppure utilizzate da altri programmi!!!! ESEMPIO DI PROGRAMMA ERRATO! #include char* negac( char*, int* ); main() { char c[4]; int d = 0; sprintf( c, "%s", "SI" ); printf( "Inizio: c vale %s e d %d\n", c, d ); negac( c, d ); /* NON SI PASSA L'INDIRIZZO DI D!!! */ printf( "Ritorno da negac: c vale %s e d %d\n", c,d ); } char* negac( char* f, int* k ) { printf( "Ingresso di negac: c vale %s e d %d\n", f, *k ); sprintf( f, "%s", "NO" ); *k+=1; } Il codice precedente causa warning in compilazione ed un grave errore di run-time in quanto cerca di accedere ad un dato che non puo' essere trovato a causa del modo errato in cui e' stato passato il secondo parametro della funzione. Nel prossimo capitolo esamineremo ancora alcune proprieta' delle funzioni e dei puntatori alle stesse.