480 likes | 660 Views
Funzioni definite dall’utente Scopo delle funzioni del C, sia fornite dal linguaggio ( di biblioteca ) sia scritte dall’utente, è quello di ricevere in ingresso uno o più valori, operare su essi e restituire direttamente un singolo valore in uscita:.
E N D
Funzioni definite dall’utente Scopo delle funzioni del C, sia fornite dal linguaggio (di biblioteca) sia scritte dall’utente, è quello di ricevere in ingresso uno o più valori, operare su essi e restituire direttamente un singolo valore in uscita: Prima di vedere le funzioni scritte dall’utente, ricordiamo quanto già visto sulla chiamata e l’uso di due funzioniprintf()e scanf(). Come già sappiamo, una funzione viene usata o chiamata indicandone il nome e passandole dei dati nelle parentesi che seguono il suo nome.
La funzione chiamata deve essere in grado di accettare i dati che le vengono passati dalla funzione chiamante, e solo dopo che sono stati ricevuti con successo, i dati possono essere manipolati per produrre un risultato utile. Una volta che sia stata definita in un programma, una funzione può essere chiamata dalle altre funzioni del programma. Naturalmente, nel creare una funzione propria, ci si deve preoccupare sia della funzione stessa, sia del modo in cui si interfaccia alle altre funzioni. Per chiarire il processo di inviare e ricevere dati, consideriamo il seguente segmento di programma, che chiama una funzionetrova_max():
Tale programma potrà essere eseguito solo dopo che sarà stata scritta e inserita in esso la funzionetrova_max(), in grado di accettare i due dati che le vengono passati e determinare il più grande di essi. trova_max() è detta funzione chiamata, mentre la funzione che esegue la chiamata, in questo caso main(), è detta funzione chiamante.
Dichiarazione (prototipo) di funzione. Prima che una funzione possa essere chiamata deve essere dichiarata, all’interno della funzione chiamante, da un’istruzione detta prototipo di funzione. Essa dichiara, nell’ordine: • il tipo di valore che la funzione chiamata restituirà alla funzione chiamante (se presente); • il tipo dei valori che la funzione chiamata si aspetta di ricevere dalla funzione chiamante. • Nel nostro esempio, il prototipo di funzione float trova_max(float, float); • dichiara che la funzionetrova_max() • restituirà un valore in virgola mobile; • si aspetta che le vengano passati due valori pure in virgola mobile.
Un prototipo di funzione può essere situato: • insieme alle istruzioni di dichiarazione delle variabili della funzione chiamante (come nel caso precedente), oppure • prima del nome della funzione chiamante, ossia prima o dopo l’istruzione#include <stdio.h>. • La forma generale dell’istruzione prototipo di funzione è: • tipo-dati-restituito nome-funzione(lista • tipi dati degli argomenti); Iltipo-dati-restituitodalla funzione deve corrispondere al tipo dati che sarà usato nella sua linea d’intestazione. Analogamente, lalista tipi dati degli argomentideve corrispondere a quelli che saranno usati nella definizione della funzione. L’uso dei prototipi di funzione permette al compilatore il controllo di errore dei tipi di parametri: se il prototipo di funzione non si accorda con i tipi dati dei parametri restituiti, contenuti nella linea d’intestazione della funzione, viene visualizzato un messaggio di errore (tipicamente TYPE MISMATCH).
Il prototipo serve anche ad assicurare la conversione di tutti gli argomenti passati alla funzione nei tipi dati degli argomenti dichiarati quando la funzione è chiamata. Chiamata. Per chiamare una funzione è sufficiente scriverne il nome e racchiudere nelle parentesi che lo seguono i dati che sono passati alla funzione (detti argomenti attuali), come nell’esempio seguente: Se uno degli argomenti in una chiamata di funzione è una varabile, la funzione chiamata riceve una copia del valore memorizzato nella variabile.
Ad es., l’istruzione: maxnum = trova_max(primonum, secnum); • chiama la funzione trova_max; • le passa i valori memorizzati nelle variabili primonum e secnum; • assegna il valore restituito dalla funzone a maxnum. • I nomi di variabili nelle parentesi sono gli argomenti attuali che forniscono i valori alla funzione chiamata. • Dopo che i valori sono stati passati, il controllo è trasferito alla funzione chiamata. • Come mostra la figura, la funzione trova_max() non riceve le variabili di nome primonum e secnum, e non ha conoscenza di questi nomi di variabile.
La funzione riceve piuttosto delle copie dei valori memorizzati in queste variabili, e deve a sua volta determinare dove memorizzare tali valori prima di compiere su essi qualsiasi operazione. Questa procedura di sicurezza garantisce che una funzione chiamata non cambi inavvertitamente i dati memorizzati in una variabile, ma cambi invece la sua copia della variabile. Perciò, a meno che non siano compiuti passi espliciti, a una funzione non è consentito cambiare i contenuti delle variabili dichiarate nella chiamata a essa.
I parametri dichiarati nella definizione della funzione sono usati per memorizzare i valori passati alla funzione quando essa viene chiamata. Come illustra la figura, la funzione trova_max tratta i suoi parametri x e y come delle variabili, la cui inizializzazione avviene al di fuori della funzione.
Definizione. Analogamente a main(), ogni funzione C consiste in due parti: un’intestazione e un corpo. • Scopo dell’intestazione è: • identificare il tipo dati del valore restituito dalla funzione; • fornire un nome alla funzione; • specificare numero, ordine e tipo degli argomenti che la funzione si aspetta. • Scopo del corpo è: • operare sui dati passati; • restituire direttamente al massimo un valore alla funzione chiamante.
Linea d’intestazione. L’intestazione di una funzione consiste in una singola linea che contiene, nell’ordine: • il tipo dati del valore restituito dalla funzione; • il nome della funzione; • i nomi e i tipi dati degli argomenti della funzione. • Dato che non è un’istruzione, ma l’inizio del codice che definisce la funzione, la linea d’intestazione termina senza “;” • Ad es., la seguente linea d’intestazione di funzione dichiara:
I nomi degli argomenti sono detti parametri o argomenti formali, mentre il nome di funzione e i parametri sono detti dichiaratore di funzione. Tutti i parametri elencati nel dichiaratore di funzione devono essere separati da virgole e i loro tipi dati devono essere specificati separatamente. Se si omette un tipo dati, il parametro viene implicitamente assunto di tipo intero. Ad es., il dichiaratore float trova_max(float x, y) non dichiara entrambi i parametri, x e y, di tipo float, ma dichiara il parametro x di tipo float, e y di tipo intero. Analogamente, se si omette il tipo dati del valore restituito, la funzione restituisce implicitamente un valore intero.
Così, entrambe le intestazioni int val_max(float x, float y) e val_max(float x, float y) definiscono una funzione val_max che restituisce un valore intero. All’interno di una intestazione di funzione si usa la parola chiavevoidper dichiarare o che la funzione non restituisce alcun valore, o che non ha argomenti. Ad es., l‘intestazione di funzione void display(int x, double y) dichiara che la funzione display() non restituisce alcun valore, mentre l’intestazione di funzione double stampa_messaggio(void) dichiara che la funzione stampa_messaggio() non ha parametri, ma restituisce un valore di tipo double.
Corpo. Dopo avere scritto l’intestazione della funzionetrova_max(), possiamo costruirne il corpo, che conterrà eventuali dichiarazioni di variabili e istruzioni C racchiuse entro la solita coppia di parentesi{ e }. In questo caso la funzione completa è: • Quando s’incontra l’istruzionereturn, • viene calcolata l’espressione entro parentesi, quindi il suo valore: • viene convertito automaticamente nel tipo dati dichiarato all’inizio della linea d’intestazione della funzione e • viene restituito alla funzione chiamante.
Dopo che il valore è stato restituito, il controllo del programma ritorna alla funzione chiamante. Quando si chiama la funzione trova_max, il parametro x è usato per memorizzare il primo valore che le viene passato, e y per il secondo. La funzione tuttavia non sa da dove provengano i valori quando è effettuata la chiamata. Osserviamo che nell’istruzione return il tipo dati della variabile restituita corrisponde esattamente al tipo dati nella linea d’intestazione della funzione. Ciò deve avvenire per ogni funzione che restituisca un valore. Il programma seguente inserisce la funzione trova_max all’interno del codice di programma presentato in precedenza.
Passaggio di vettori (1). Per passare a una funzione singoli elementi di un vettore, essi vanno inseriti come variabili con indici nell’elenco degli argomenti nella chiamata alla funzione. Ad es., la chiamata di funzione trova_min(voti[2], voti[6]); passa i valori degli elementi voti[2] e voti[6] alla funzionetrova_min(). Analogamente, per passare a una funzione un vettore completo si inserisce il suo nome nell’elenco degli argomenti. Ad es., il vettore voti viene passato alla funzionetrova_max con la chiamata di funzione trova_max(voti); Quando si passa un vettore completo a una funzione, essa riceve accesso al vettore effettivo, anziché a una copia dei suoi valori (come avviene quando vengono passati singoli elementi).
Infatti, come sappiamo, quando si passa un singolo scalare la funzione chiamata riceve una copia del valore memorizzato nella variabile, e ciò succede anche se si passa un singolo elemento di un vettore. Il passaggio di un vettore in questa maniera richiederebbe l’esecuzione di una copia completa e separata di tutte le sue componenti; nel caso di vettori grandi l’esecuzione di una copia a ogni chiamata di funzione sprecherebbe la memoria del computer, consumerebbe il tempo di elaborazione e vanificherebbe lo sforzo di ritornare cambiamenti multipli di elementi da parte del programma chiamato. Per evitare questi problemi, la funzione chiamata ha accesso diretto al vettore originale, cosicché ogni cambiamento da essa eseguito è apportato direttamente al vettore stesso.
Se, ad es., abbiamo dichiarato il vettore di 5 interiint num[5];possiamo dichiarare la funzione void trova_max(int[5]) ed eseguire la seguente chiamata: trova_max(num); L’intestazione della funzione chiamata potrebbe allora essere: void trova_max(int val[5]) In questa intestazione il nome dell’argomento (val) è locale alla funzione, ma si riferisce ancora al vettore originario (num) creato al di fuori della funzione.
Passaggio di matrici. Il passaggio a una funzione di una matrice avviene in modo analogo a quello di un vettore a una dimensione: in particolare, anche adesso la funzione chiamata riceve accesso all’intera matrice. Ad es., la chiamata mostra(val);rende disponibile l’intera matrice val alla funzione di nome mostra(), cosicché ogni cambiamento eseguito da essa sarà fatto direttamente suval. Se è stata dichiarata la matricechar codice[26][9]; e la funzione char ottieni(int[26] [9]) è valida la seguente chiamata: ottieni(codice); mentre la funzione chiamata avrà un’intestazione del tipo: char ottieni(char chiave[26][9]) In essa il nome dell’argomento (chiave) è locale alla funzione, e tuttavia si riferisce ancora alla matrice originaria creata fuori da essa.
Se la matrice è globale, non è necessario passarla alla funzione, che può farvi riferimento tramite il nome globale. Il programma seguente illustra il passaggio di una matrice locale a una funzione che ne visualizza i valori. #include <stdio.h> void main(void) { int val[3][4] = {8,16,9,52, 3,15,27,6, 14,25,2,10}; void mostra(int[3][4]); /* prototipo */ mostra(val); /* chiamata */ } void mostra(int num[3][4]) /* intestazione */ { int ri, col; for (ri = 0; ri < 3; ++ri) { for(col= 0; col < 4; ++col) printf("%4d", num[ri][col]); printf("\n"); } }
#include <stdio.h> void main(void) { int val[3][4] = {8,16,9,52, 3,15,27,6, 14,25,2,10}; void mostra(int[3][4]); mostra(val); } void mostra(int num[3][4]) { int ri, col; for (ri = 0; ri < 3; ++ri) { for(col= 0; col < 4; ++col) printf("%4d", num[ri][col]); printf("\n"); } }
Ecco l’uscita prodotta: Il programma crea un solo vettore, conosciuto come val inmain()e comenuminmostra(), per cuival[0][2]enum[0][2]si riferiscono allo stesso elemento.
Osservazione. Anche adesso la dichiarazione degli argomenti pernumcontiene un’informazione non necessaria alla funzione, ossia il numero di righe del vettore. Perciò è corretta anche l’intestazione di funzione mostra(int num[][4]) La ragione per cui il numero di colonne vada indicato, mentre quello di righe è opzionale diventa ovvia quando si consideri che tutti gli elementi del vettore sono memorizzati in memoria in modo sequenziale, a partire dall’elementoval[0][0]. Per accedere a un singolo elemento del vettoreval, il computer si sposta di un certo numero di byte, detto offset, a partire dall’inizio del vettore, ossia aggiunge all’indirizzo della locazione iniziale del vettore un opportuno offset.
Per calcolare tale offset, supponiamo che: • un intero richieda due byte di memoria, • si voglia accedere all’elementoval[1][3]. • Come risulta dal seguente schema, l’offset è in questo caso di 14 byte.
#include <stdio.h> void main(void) { int num[5] = {2, 18, 1, 27, 16}; void trova_max(int [5]); /*prototipo*/ trova_max(num); } void trova_max(int val[5]) { int i, max = val[0]; for (i = 1; i <= 4; ++i) if (max < val[i]) max = val[i]; printf("Il valore massimo è %d", max); } Componente massima. Questi concetti sono applicati nel seguente programma, dove si chiama una funzione che trova la componente di valore massimo di un vettore:
Osservazioni. 1. Il prototipo della funzione trova_max all’interno dimain() dichiara che trova_max non restituisce un valore, in quanto la stampa del massimo è effettuata dalla funzione chiamata. 2. Il programma crea un solo vettore, che in main() è noto come num, in trova_max è noto come val. Come mostra la figura, entrambi i nomi si riferiscono allo stesso vettore.
Passaggio di vettori (2). La dichiarazione degli argomenti nella intestazione di trova_max() contiene in effetti un’informazione aggiuntiva non necessaria alla funzione. Tutto ciò che trova_max() deve sapere è che l’argomento val si riferisce a un vettore di interi; dato che esso è stato creato in main() e non richiede spazio aggiuntivo in trova_max(), la dichiarazione pervalpuò omettere il numero dei suoi elementi. Quindi è corretta anche l’intestazione: trova_max(int val[]) Questa forma si comprende meglio se si considera che quando si chiama trova_max() le viene passato un solo parametro, ossia l’indirizzo di partenza del vettore num, che è &num[0] per tale ragione nella dichiarazione per il vettore val non è necessario inserire il numero dei suoi elementi.
In effetti: • nella dichiarazione degli argomenti è meglio • omettere il numero degli elementi di un vettore • Perciò la forma più generale della funzione trova_max è quella usata nel programma seguente; essa dichiara che trova_max • restituisce un valore intero, • si aspetta come argomenti l’indirizzo di partenza di un vettore di interi e il numero dei suoi elementi (che usa come limite per la ricerca dell’elemento massimo, eseguita dal ciclo for).
#include <stdio.h> void main(void) { int num[5] = {2, 18, 1, 27, 16}; int trova_max(int [], int); /* prototipo */ printf("Il valore massimo è %d", trova_max(num,5)); } int trova_max(int val[], int num_elem) { int i, max = val[0]; for (i=1 ; i<num_elem ; ++i) if(max < val[i]) max = val[i]; return(max); }
#include <stdio.h> void main(void) { int cont; double fahren; double tempconv(double); /* prototipo */ for (cont = 1; cont <= 4; ++cont) { printf("Scrivi i gradi Fahrenheit: "); scanf("%lf", &fahren); printf("Equiv. Celsius: %6.2f\n\n", ); } } tempconv(fahren) Conversione Fahrenheit-Celsius. Il seguente programma chiede di scrivere 4 temperature in gradi Fahrenheit e le converte in gradi Celsius: double tempconv(double in_temp) /* intestazione */ { return( (5.0/9.0) * (in_temp - 32.0) ); }
Fattoriale ricorsivo. Come altro esempio, osserviamo il programma seguente, che usa una funzione definita dall’utente per calcolare il fattoriale di un numero intero secondo la sua definizione ricorsiva. Osserviamo che la definizione della funzione chiamata potrebbe trovarsi anche prima della funzione chiamante, e situarla prima o dopo è solo una questione di scelta.
Serie di Fibonacci. Un altro esempio di funzione ricorsiva è costituito dal calcolo della serie di Fibonacci, scoperta dal matematico Leonardo Pisano nel 1202, come soluzione al problema della riproduzione dei conigli in circostanze ideali. • Consideriamo una coppia di conigli neonati, maschio e femmina, in grado di riprodursi all’età di un mese, con un periodo di gestazione anch’esso di un mese. • Perciò, alla fine del secondo mese, la coppia iniziale ha generato una seconda coppia di conigli. • Supponiamo che: • i conigli non muoiano mai, e che • la femmina generi sempre una nuova coppia (maschio e femmina) di conigli ogni mese, a partire dal secondo mese.
La domanda che si pose Fibonacci fu: quante coppie di conigli ci saranno dopo un anno? La risposta si trova considerando che: • alla fine del 1° mese i due conigli si accoppiano, ma c’è ancora 1 sola coppia; • alla fine del 2° mese la femmina genera una 1^ coppia, cosicché ci sono 2 coppie; • alla fine del 3° mese la femmina di partenza genera una 2^ coppia, cosicché ci sono 3 coppie; • alla fine del 4° mese la femmina di partenza genera una 3^ coppia, mentre quella nata 2 mesi prima genera le sua 1^ coppia, cosicché ci sono 5 coppie; • . . . . .
Perciò il numero di coppie di conigli all’inizio di ogni mese è: 1, 1, 2, 3, 5, 8, 13, 21, 34, ... Questa successione di numeri interi si può generare in modo ricorsivo, anzi essa costituisce la prima sequenza numerica ricorsiva conosciuta in Europa. Infatti ogni termine è uguale alla somma dei due precedenti: fibo(n) = fibo(n-1) + fibo(n-2) La successione ha riscontri in vari fenomeni naturali e descrive, tra l’altro, una particolare forma di spirale. Inoltre il rapporto tra un termine di Fibonacci e il precedente converge verso il valore 1,61803 39887... (o verso il suo reciproco 0,61803 39887, se si considera il rapporto tra un termine e il successivo). Anche questo numero ha diversi riscontri in natura, ed è stato definito rapporto aureo o divina proportione.
#include <stdio.h> long fibo(int); int main() { int numero; long risult; printf("Scrivi un numero: "); scanf("%d", &numero); risult = fibo(numero); printf("Fibonacci( %d ) = %ld\n", numero, risult); } long fibo(int n) { if (n==0 || n==1) return n; else return fibo(n-1)+fibo(n-2); } Il programma seguente genera il termine n-esimo della successione di Fibonacci, a partire dal valore di n, chiamando una funzione ricorsiva:
Ecco la sua uscita: Scrivi un numero: 0 Fibonacci( 0 ) = 0 Scrivi un numero: 1 Fibonacci( 1 ) = 1 Scrivi un numero: 2 Fibonacci( 2 ) = 1 Scrivi un numero: 3 Fibonacci( 3 ) = 2 Scrivi un numero: 4 Fibonacci( 4 ) = 3 Scrivi un numero: 5 Fibonacci( 5 ) = 5 Scrivi un numero: 6 Fibonacci( 6 ) = 8
Una leggera modifica del programma precedente permette di stampare tutti i termini della successione di Fibonacci fino a quello di numero d’ordine indicato dall’utente. #include <stdio.h> long fibo(int); int main() { int i, numero; printf("Scrivi un numero: "); scanf("%d", &numero); for (i = 0; i <= numero; i++) printf("\nFibonacci( %2d ) = %8ld", i, fibo(i)); } long fibo(int n) { if (n==0 || n==1) return n; else return fibo(n-1)+fibo(n-2); }
Serie di Fibonacci. Calcolo iterativo. La precedente definizione ricorsiva della funzione fibo() è interessante perché, a ogni sua chiamata, essa chiama se stessa due volte. Vediamo quante volte la funzione è chiamata per piccoli valori del numero d’ordine dell’elemento richiesto.
La chiamata della funzione numerose volte, o per elevati valori del numero d’ordine, appesantisce quindi l’esecuzione di un programma. Perciò può essere utile scriverne una versione iterativa. Conviene scrivere dapprima un programma che calcoli e stampi una tabella di numeri di Fibonacci in modo iterativo, come il seguente, che usa un vettore di interi lunghi per memorizzare i numeri, un ciclo for per il calcolo e un ciclo for per la stampa. #include <stdio.h> main() { long fib[24]; int i; fib[0] = 0; fib[1] = 1; for(i = 2; i < 24; i++) fib[i] = fib[i-1] + fib[i-2]; for (i = 0; i < 24; i++) printf("Fibonacci( %2d ) = %8ld\n", i, fib[i]); }
Adesso modifichiamo leggermente questo programma, in modo che, come prima, chieda il numero d’ordine di un elemento della successione e lo stampi. #include <stdio.h> main() { int i, numero; printf("Scrivi il n° d'ordine: "); scanf("%d", &numero); long fib[numero]; fib[0] = 0; fib[1] = 1; for (i = 2; i <= numero; i++) fib[i] = fib[i-1] + fib[i-2]; printf("Fibonacci( %d ) = %ld\n", numero, fib[numero]); }
Naturalmente le funzioni possono operare anche sulle liste concatenate di strutture, ad es. per compiere un ciclo entro un’intera lista per stamparne i valori. Il programa seguente definisce e popola la lista concatenata di strutture vista in precedenza, quindi chiama la funzione mostra() per visualizzarne gli elementi. La funzione mostra() contiene un ciclo while che usa gli indirizzi contenuti nel membro puntatore di ogni struttura per compiere un ciclo attraverso la lista e visualizzare in successione i dati contenuti in ogni struttura.
#include <stdio.h> struct Tipo_tel { char nome[30]; char num_tel[15]; struct Tipo_tel *prossindir; }; void main(void) { struct Tipo_tel t1 = {"Aloisi, Sandro","0432 174973"}; struct Tipo_tel t2 = {"Dolan, Edith","02 385602"}; struct Tipo_tel t3 = {"Lisi, Giovanni","0556 390048"}; struct Tipo_tel *primo; void mostra(struct Tipo_tel *); /* prototipo */ primo = &t1; t1.prossindir=&t2; t2.prossindir=&t3; t3.prossindir=NULL; mostra(primo); /*chiamata di funzione*/ }
void mostra(struct Tipo_tel *contenuto) /*intestazione*/ { while (contenuto != NULL) { printf("%-30s %-20s\n",contenuto->nome,contenuto->num_tel); contenuto=contenuto->prossindir; } return; } Ecco l’uscita prodotta: Aloisi, Sandro 0432 174973 Dolan, Edith 02 385602 Lisi, Giovanni 0556 390048
Osservazioni. Il programma precedente illustra l’uso degli indirizzi contenuti in una struttura per accedere ai membri della struttura che la segue nella lista. Quando si chiama la funzione mostra(), le viene passato il valore memorizzato nella variabile primo; dato che primo è una variabile puntatore, il valore effettivamente passato è un indirizzo (quello della struttura t1). mostra() memorizza il valore passatole nell’argomento contenuto. Per una corretta memorizzazione dell’indirizzo passato,contenuto è dichiarato come un puntatore a una struttura di tipo Tipo_tel. mostra() esegue un ciclo while attraverso le strutture concatenate, a partire da quella il cui indirizzo è in contenuto. La condizione controllata nell’istruzione while confronta il valore che si trova in contenuto(che è un indirizzo) con il valore NULL.
Per ogni indirizzo valido: • sono visualizzati i membri nome e numero di telefono della struttura indirizzata, quindi • l’indirizzo che si trova in contenuto viene aggiornato con quello che si trova nel membro puntatore della struttura corrente, quindi • si controlla di nuovo l’indirizzo in contenuto, e il processo continua fino a quando l’indirizzo non sia uguale al valore NULL. mostra() non “sa” nulla circa i nomi delle strutture dichiarate in main(), e neppure quante strutture esistano, ma si limita a compiere dei cicli attraverso la lista concatenata, struttura per struttura, fino a che incontra l’indirizzo NULL di fine lista. Dato che il valore di NULL è zero, la condizione controllata può essere sostituita dall’espressione equivalente !contenuto. Uno svantaggio del programma precedente è che in main() sono definite per nome esattamente tre strutture, per le quali viene riservata memoria in fase di compilazione. Se fosse necessaria una quarta struttura, essa andrebbe dichiarata e il programma ricompilato.
Vedremo più avanti come fare allocare e rilasciare dinamicamente al computer la memoria per le strutture in fase di esecuzione, via via che serva memoria. La memoria per una nuova struttura verrà creata solo quando si debba aggiungere una nuova struttura alla lisa, e mentre il programma è in esecuzione. Analogamente, quando una struttura non è più necessaria e può essere cancellata dalla lista, la memoria per un record cancellato sarà rilasciata e restituita al computer.