Introduzione Perché i sottoprogrammi? per riutilizzare codice già scritto se si devono ripetere tante volte le stesse operazioni in punti diversi di un programma, è meglio fattorizzare il codice da ripetere in un sottoprogramma, e richiamare il sottoprogramma più volte per astrarre delle operazioni complesse se si incapsula del codice complesso in un sottoprogramma, il programma globale può essere strutturato in macro-operazioni un codice ben strutturato in sottoprogrammi è più facile da correggere, modificare, mantenere per estendere le operazioni del linguaggio per definire nuove operazioni su tipi predefiniti, e per definire le operazioni sui tipi definiti dall'utente Approfondiamo quest ultimo aspetto Abbiamo visto come definire nuovi tipi in C mediante la dichiarazione typedef Con una dichiarazione typedef noi definiamo il dominio dei valori di un nuovo tipo, ma tipo = valori + operazioni, ci manca ancora di definire le operazioni usiamo i sottoprogrammi per arricchire il C con nuove operazioni, definite dall'utente 1
Un esempio Supponiamo di avere le seguenti dichiarazioni #define MaxNumFatture 10000; #define MaxNumCosti 10000; typedef char String[20]; typedef struct { String Destinatario; int Importo; Data DataEmissione; DescrizioneFatture; typedef struct { String Destinatario; int Importo; Data DataSpesa; DescrizioneCosti; typedef struct { int NumFatture; DescrizioneFatture Sequenza[MaxNumFatture]; ElencoFatture; typedef struct { int NumCosti; DescrizioneCosti Sequenza[MaxNumCosti]; ElencoCosti;... ElencoFatture archiviofatture; ElencoCosti archiviocosti; int risultatodigestione; 2
Esempio (cont.) Ci piacerebbe realizzare operazioni come calcola la somma di tutte le fatture dell'archivio oppure calcola la somma di tutti i costi sostenuti per esempio per scrivere il guadagno netto è pari al fatturato totale meno le spese sostenute ci piacerebbe avere: risultatodigestione = FatturatoTotale(archivioFatture) SommaCosti(archivioCosti); Al momento, però, noi al massimo sappiamo scrivere una cosa del tipo: fatturato = 0; for(i = 0; i < ArchivioFatture.NumFatture; i++){ fatturato = fatturato + ArchivioFatture.Sequenza[i].Importo; costi= 0; for(i = 0; i<archiviocosti.numcosti; i++){ costi = costi + ArchivioCosti.Sequenza[i].Importo; risultatodigestione = fatturato costi; Il meccanismo che viene usato per astrarre questo codice in una operazione a parte è quello dei sottoprogrammi Un sottoprogramma è un pezzo di codice che risolve un sottoproblema specifico un sottoprogramma realizza un sottoalgoritmo, cioè risolve un sottoproblema del problema principale Un sottoprogramma è opportunamente incapsulato ed isolato dal resto del codice mediante una apposita dichiarazione 3
Struttura di un programma C: versione completa Un programma C è fatto di: direttive #include, #define,... parte dichiarativa globale main La parte dichiarativa globale contiene: dichiarazione di elementi condivisi tra il main e i sottoprogrammi costanti, variabili, ecc. definizioni di sottoprogrammi in realtà il main è un sottoprogramma speciale tra tutti i sottoprogrammi Il main e gli altri sottoprogrammi possono usare: gli elementi dichiarati nella parte dichiarativa globale gli elementi dichiarati nella loro propria parte dichiarativa Per esempio, se nella parte dichiarativa globale si mette la dichiarazione int var_g; la variabile var_g può essere usata da tutti i sottoprogrammi, incluso il main 4
Le funzioni Matematicamente, una funzione è un oggetto che ha: un dominio un codominio Una funzione, a fronte di valori nel suo dominio, ritorna un valore nel codominio fino ad ora le operazioni che abbiamo considerato sono tutte delle funzioni (per esempio +, -, *, ecc.) a fronte dei valori 3 e 5, la funzione + restituisce il valore 8 Di fatto una funzione ha: dei valori in ingresso un valore in uscita I valori in ingresso alla funzione + sono gli addendi, ed il valore in uscita è il risultato della somma Avvertenza: spesso in C si usa il termine funzione come sinonimo di sottoprogramma, ma non è un uso preciso del termine una funzione è un genere particolare di sottoprogramma, ne vedremo un altro più avanti Definizione di una funzione: testata (header) tra parentesi graffe: parte dichiarativa (detta parte dichiarativa locale) contiene dichiarazioni di costanti, variabili, locali parte esecutiva (detta corpo della funzione) contiene una sequenza di istruzioni 5
Testata e corpo di una funzione La testata di una funzione si compone, nell'ordine, di: tipo del risultato identificatore del sottoprogramma cioè il suo nome lista dei parametri (detti parametri formali) a cui la funzione viene applicata Ogni dichiarazione di parametro è fatta di un identificatore di tipo seguito dall'identificatore (cioè dal nome) del parametro Alcune osservazioni: il tipo del risultato è il codominio della funzione i parametri formali sono il dominio della funzione il tipo del risultato può essere qualunque (predefinito o definito dall'utente), tranne che di tipo array può però essere di tipo puntatore a qualsiasi tipo Esempi di testate (non di definizioni complete) int FatturatoTotale (ElencoFatture par) boolean Precede (StringaCaratteri par1, StringaCaratteri par2) boolean Esiste (int par1, SequenzaInteri par2) /* Stabilisce se il primo parametro, di tipo intero, * appartiene all'insieme di interi contenuti nel * secondo parametro: una sequenza di interi */ MatriceReali10Per10 *MatriceInversa (MatriceReali10Per10 *par) La definizione di una funzione è completata da dichiarazioni locali costanti, variabili, esattamente come era per il main le variabili dichiarate all'interno di una funzione sono dette variabili locali corpo della funzione costruito secondo le stesse regole della parte esecutiva del main in più può contenere una (o più) istruzioni di return 6
istruzione return ed esempi di funzioni Sintassi dell'istruzione di return: return espressione il valore di espressione è il risultato della funzione ci può essere più di una istruzione return nel corpo di una funzione quando, durante l'esecuzione del corpo di una funzione, si incontra l'istruzione di return, l'esecuzione della funzione termina e viene restituito il valore dell'espressione Esempio: funzione che calcola il valore totale degli importi di un elenco di fatture int FatturatoTotale (ElencoFatture par){ int i; int totale = 0; for(i = 0; i < par.numfatture; i++){ totale = totale + par.sequenza[i].importo; return totale; Altro esempio: funzione che calcola la radice quadrata intera di un numero intero int RadiceIntera (int par) { int cont; cont = 0; while (cont*cont <= par){ cont = cont + 1; return (cont 1); 7
Chiamata delle funzioni Dopo che è stata definita, una funzione può essere chiamata (o invocata) ogni invocazione di una funzione restituisce un valore, che dipende dai valori dei parametri della funzione al momento dell'invocazione ci sono 2 attori in gioco: chi chiama la funzione (il chiamante), e la funzione chiamata (il chiamato) chi chiama la funzione è il main, oppure un altro sottoprogramma Sintassi di chiamata: IdentificatoreFunzione ( lista dei parametri attuali ) Esempi: FatturatoTotale(archivioFatture) Radice(num) Radice(13) Parametri attuali: valore attribuito ai parametri della funzione al momento della chiamata contrapposti ai parametri formali, che sono i nomi (le scatole vuote ) dichiarati nella testata della funzione per esempio il valore di num e 13 sono parametri attuali per il parametro formale par della funzione Radice Quando per esempio invoco Radice(13), il valore 13 viene messo dentro al parametro par è di fatto un assegnamento 8
Parametri attuali e formali La corrispondenza tra parametri formali e parametri attuali è posizionale: il primo parametro attuale nella chiamata viene assegnato al primo parametro formale nella dichiarazione il secondo parametro attuale va nel secondo parametro formale... Il numero di parametri attuali deve essere uguale al numero di parametri formali altrimenti viene segnalato un errore in fase di compilazione Un parametro attuale è una espressione, che può a sua volta includere una chiamata di funzione Il tipo dei parametri attuali (cioè dell'espressione corrispondente) deve essere compatibile con il tipo dei parametri formali sono possibili le conversioni implicite di tipo In diversi momenti dell'esecuzione di un programma ci possono essere diverse invocazioni della stessa funzione con diversi parametri attuali Esempi di invocazioni: x = sin(y) cos(pigreco alfa); x = cos(atan(y) beta); x = sin(alfa); y = cos(alfa) sin(beta); z = sin(pigreco) + sin(gamma); risultatodigestione = FatturatoTotale(archivioFatture) - SommaCosti(archivioCosti); det1 = Determinante(matrice1); det2 = Determinante(MatriceInversa(matrice2)); totaleassoluto = Sommatoria(lista1) + Sommatoria(lista2); elencoordinato = Ordinamento(elenco); ordinatialfabeticamente = Precede(nome1, nome2); 9
Prototipo delle funzioni Una funzione può essere chiamata solo se è definita o se è dichiarata Attenzione: definita dichiarata Definizione testata { dichiarazioni locali corpo Dichiarazione (detta prototipo) testata; Esempi di prototipi di funzione: int FatturatoTotale (ElencoFatture par); int RadiceIntera (int par); Punto PuntoMedio(Segmento seg); La dichiarazione di una funzione può andare in una qualunque parte dichiarativa tra quelle viste: parte dichiarativa globale parte dichiarativa locale di una funzione parte dichiarativa del main Dichiarare una funzione è utile quando una chiamata della funzione precede la sua definizione nel file da compilare, oppure quando si una una funzione definita in un file separato (per esempio le funzioni di libreria) Dichiarare le funzioni aiuta il compilatore ed è buono stile di programmazione Per riassumere, una parte dichiarativa (globale, locale ad un sottoprogramma, oppure locale al main) può contenere (in ordine sparso): dichiarazioni di costanti dichiarazioni di tipo dichiarazioni di variabili prototipi delle funzioni 10
Esecuzione di funzioni e passaggio parametri Cominciamo con un esempio: /* Programma Contabilità */ /* Parte direttiva */ #include <stdio.h> #define MaxNumFatture 1000 /* Parte dichiarativa globale */ typedef char String [30]; typedef struct { String Indirizzo; int ammontare; Data DataFattura; DescrizioneFatture; typedef struct { int NumFatture; DescrizioneFatture Sequenza[MaxNumFatture]; ElencoFatture; main() { ElencoFatture archiviofatture1, archiviofatture2; int fatt1, fatt2, fatt; /* Prototipo della funzione FatturatoTotale */ int FatturatoTotale(ElencoFatture par);... fatt1 = FatturatoTotale(archivioFatture1); fatt2 = FatturatoTotale(archivioFatture2); fatt = fatt1 + fatt2;... /* Fine del main del programma Contabilità */ int FatturatoTotale (ElencoFatture par) { int totale, i;... return totale; 11
Esecuzione (cont.) E' come se ci fossero 2 esecutori, uno per il main, ed uno per la funzione FatturatoTotale Ogni esecutore ha un insieme di variabili locali (detto ambiente di esecuzione) ambiente di esecuzione per il main: archiviofatture1 e archiviofatture2, di tipo ElencoFatture fatt1, fatt2, fatt3, di tipo int ambiente di esecuzione per FatturatoTotale: par, di tipo ElencoFatture totale ed i, entrambe di tipo int una variabile anonima (che non può essere referenziata in nessun modo) in cui viene memorizzato il valore ritornato dalla chiamata archiviofatture1 archiviofatture2 fatt1 fatt2 par totale i valore da ritornare fatt3 esecutore del main esecutore di FatturatoTotale 12
Esecuzione (cont.) archiviofatture1 par archiviofatture2 fatt1 fatt2 totale i valore da ritornare fatt3 Effetto dell'esecuzione di: fatt1 = FatturatoTotale(archivioFatture1); l'esecutore del main vede che a destra di = c'è un'invocazione alla FatturatoTotale con parametro attuale archiviofatture1 viene creato l'esecutore per la funzione FatturatoTotale, con il suo ambiente di esecuzione avviene il passaggio dei parametri il valore del parametro attuale viene copiato nel corrispondente parametro formale viene ceduto il controllo alla macchina asservita l'esecuzione dell'esecutore del main viene sospesa viene eseguito il corpo della funzione fino a return la variabile anonima contiene il valore da ritornare viene restituito il controllo all'esecutore del main l'esecutore del main preleva il valore della variabile anonima questo valore diventa il valore dell espressione: FatturatoTotale(ArchivioFatture1) il valore viene assegnato alla variabile fatt1 13
Le procedure Non tutte le operazioni interessanti sono descrivibili astrattamente da funzioni matematiche a volte si vuole: effettuare qualche azione cambiare valore di variabili Per esempio: stampare un elenco di fatture non c è alcun valore da calcolare va fatto in modo parametrico (diversi elenchi in diversi momenti) inserire una nuova fattura in un archivio di fatture preesistente aggiornamento di variabile, non calcolo di valore ordinare un array di interi ar effetto: permutare i valori presenti in ar NON calcolare un valore di tipo array di interi modificare le coordinate di un vertice di una linea spezzata Per queste operazioni si usa diverso tipo di sottoprogramma: sottoprogramma procedurale o procedura Sintatticamente, una procedura si distingue da una funzione dal tipo del risultato: una procedura ha come tipo del risultato il tipo void Il tipo void è un tipo predefinito fittizio, non possiede alcun valore né operazione il tipo void è usabile anche come tipo per parametri formali di sottoprogrammi senza parametri La chiamata di una procedura è un istruzione che non produce alcun valore: IdentificatoreProcedura( lista dei parametri attuali ); è quindi un'istruzione singola, a parte, non può essere inserita in un'espressione si noti il ';' di chiusura: questa è una istruzione completa 14
Esempio di procedura Operazione InserisciFattura: /*Programma Contabilità */ #include<stdio.h> #define MaxNumFatture 1000... typedef struct { String Indirizzo; int ammontare; Data DataFattura; DescrizioneFatture; typedef struct { int NumFatture; DescrizioneFatture Sequenza[MaxNumFatture]; ElencoFatture; ElencoFatture archfatture; /* variabile condivisa */ main() { Data dataodierna; DescrizioneFatture fattura1, fattura2; void InserisciFattura(DescrizioneFatture fatt); boolean Precede(Data d1, Data d2);... /* Qui va la sequenza di istruzioni (non mostrata) * che leggono i dati di una fattura in fattura1 */ InserisciFattura(fattura1);... /* Qui va la sequenza di istruzioni (non mostrata) * che leggono i dati di una fattura in fattura2 */ if (Precede(fattura2.DataFattura, dataodierna)) InserisciFattura(fattura2);... void InserisciFattura(DescrizioneFatture fatt){ if (archfatture.numfatture == MaxNumFatture){ printf("l'archivio è pieno.\n"); else { archfatture.sequenza[archfatture.numfatture] = fatt; archfatture.numfatture = archfatture.numfatture + 1; 15
Esecuzione dell'esempio archiviofatture Ambiente globale dataodierna... fattura1 fattura2 fatt Ambiente locale di InserisciFattura Ambiente di main del programma Contabilità oltre all'ambiente del main e a quello della procedura c è l'ambiente globale del programma contiene la variabile archiviofatture di tipo ElencoFatture L'esecutore della procedura InserisciFattura accede all ambiente globale modificandolo La variabile archiviofatture è per la procedura una variabile globale Al della sua esecuzione, l'esecutore di InserisciFattura cede il controllo all'esecutore principale senza restituire alcun risultato l' effetto desiderato è la modifica della variabile globale Osservazione: le varie procedure e funzioni (main compreso) non possono accedere agli ambienti locali altrui la comunicazione avviene o passandosi parametri e risultati o attraverso l ambiente globale/comune 16