Sottoprogrammi e funzioni Parte 5 Sottoprogrammi, funzioni, e passaggio dei parametri. Corso A: Prof. Stefano Berardi http://www.di.unito.it/~stefano Corso B: Prof. Ugo de’ Liguoro http://www.di.unito.it/~deligu
Finalmente arrivano le funzioni …
Indice Parte 5: Funzioni I sottoprogrammi al tempo del GOTO. I sottoprogrammi ai tempi della programmazione strutturata: le funzioni. Variabili locali e globali. Passaggio per riferimento. Come si descrive una funzione? Pre- e Post- condizioni. Le funzioni vengono usate in C++ per scomporre un problema in sottoproblemi e facilitarne così la soluzione.
1. I sottoprogrammi Salvo per le operazioni elementari (ad es. le operazioni aritmetiche) ad ogni operazione di un programma corrisponde un gruppo di istruzioni in C++. Un’operazione più volte ripetuta in un programma (es.: disegnare un cerchio con certe caratteristiche) può essere realizzata da un gruppo di istruzioni che viene eseguito ogni volta che serve: un sottoprogramma. L’idea di sottoprogramma corrisponde all’idea di scomporre un problema in sottoproblemi. 5-Funzioni
Perché i sottoprogrammi? La scomposizione di un programma in sottoprogrammi rende un programma più leggibile e (di solito) più breve, ma il suo scopo principale è quello di correggere separatamente anziché tutte insieme le diverse parti del programma. I sottoprogrammi sono indispensabili per i programmi lunghi. Non è effettivamente possibile scrivere un programma di grandi dimensioni senza scomporlo in sottoprogrammi, perché trovare un errore nascosto in un lungo blocco di istruzioni richiede troppo tempo. 5-Funzioni
Esempi di sottoprogrammi Vedremo nel Laboratorio del corso come anche programmi lunghi poche pagine, come l’implementazione dell’algoritmo di Gauss-Jordan e degli Automi Cellulari, e addirittura programmi brevi come uno che disegna coriandoli diventano molto più semplici da scrivere se scomposti in sottoprogrammi. Un’avvertenza su questa lezione. Per semplicità di esposizione, nei prossimi lucidi scomporremo in sottoprogrammi anche alcuni programmi brevissimi, che non richiederebbero affatto tale scomposizione. 5-Funzioni
Un sottoprogramma per il massimo al tempo dei GOTO 100 LET X = 3 110 LET Y = 5 120 GOSUB 400 130 PRINT MASSIMO ... 400 IF X > Y THEN 430 410 LET MASSIMO = Y 420 GOTO 440 430 LET MASSIMO = X 440 RETURN Assegno 3, 5 a X, Y “Chiamata” al sottopro- gramma: “salto” alle istruzioni del sottopro-gramma per il calcolo di max(x,y) “Ritorno” dal sottoprogram-ma per il massimo: “salto” all’indietro, ora MASSIMO = max(3,5) = 5 5-Funzioni
Come si ritornava al programma principale 120 GOSUB 400 130 ... 400 ... 440 RETURN Quando si eseguiva l’istruzione 120 che “chiama” il sottoprogramma, prima di saltare alla linea 400 veniva memorizzato in una variabile l’indirizzo 120 da cui si proveniva. Quando poi si eseguiva RETURN (ultima istr. 440 del sottoprogramma), si eseguiva un JUMP o “salto” all’istruzione 130, quella immediatamente successiva all’istruzione 120 ricordata. 5-Funzioni
I parametri dei sottoprogrammi La X e la Y sono dette i valori in ingresso del sottoprogramma,MASSIMO è detto il valore in uscita. X, Y sono detti parametri del sottoprogramma I sottoprogrammi avevano un difetto: supponiamo di aver gia’ utilizzato X, Y altrove per salvare dei valori 100 LET X = 3 110 LET Y = 5 120 GOSUB 400 130 PRINT MASSIMO ... 400 IF X>Y THEN 430 410 LET MASSIMO = Y 420 GOTO 440 430 LET MASSIMO = X 440 RETURN L’uso di variabili X, Y per “passare” i valori 3, 5 verso un sottoprogramma provocava la perdita dei valori precedenti di X, Y. 5-Funzioni
2. I sottoprogrammi ai tempi della programmazione strutt. Una funzione è un sottoprogramma con parametri propri (non appartenenti al programma principale) attraverso le quali riceve l’ingresso (e a volte può anche restituire l’uscita). L’uso di un sottoprogramma con parametri propri evita il rischio di cancellare senza volerlo il contenuto di altre variabili del programma. Questa era una fonte comune di errori quando i sottoprogrammi non avevano parametri propri. 5-Funzioni
I sottoprogrammi ai tempi della programmazione strutt. Una “chiamata” a una funzione produce un salto alle righe che contengono la funzione, mentre l’indirizzo dell’istruzione che chiama la funzione viene memorizzato. Terminata l’esecuzione della funzione, il flusso del programma riprende dall’istruzione successiva alla quella dove era avvenuta la chiamata. Tutto questo oggi avviene senza dover scrivere esplicitamente istruzioni di salto né indirizzi: ci pensa il compilatore ad aggiungerli nell’eseguibile. 5-Funzioni
Funzioni: sintassi <tipo> <nome funzione> (<lista param.>) {<corpo>} <tipo> è il tipo del valore restituito (si usa il tipo void (vuoto) se non viene restituito alcun valore) <lista param.> (eventualmente vuota) è una sequenza di dichiarazioni di variabili, separate da virgole, della forma: <tipo> <nome variabile> int max (int x, int y) { if (x > y) return x; else return y; } Tipo valore di ritorno Lista parametri Valore di ritorno: viene definito da un comando return. Il suo tipo deve essere uguale al tipo del valore di ritorno corpo della funzione
Funzioni: dichiarazioni, definizioni e chiamate Le funzioni in C++ compaiono in tre modi: Dichiarazioni (fuori dal main). Corpo ridotto a “;” double sq(double x); Una dichiarazione precisa solo i tipi della funzione. Definizioni (fuori dal main). Sono dichiarazioni seguite dal corpo {…} della funzione. double sq(double x) {return x*x; /*corpo funzione*/} Chiamate (nel main o in una funzione). Devono essere precedute almeno dalla dichiarazione, non dichiarano i tipi: scrivete b = sq(a); e non: b = sq(double a);
I parametri di una dichiarazione o definizione sono detti “formali” La dichiarazione o definizione di una funzione sta prima o dopo il main e richiede i tipi int max (int x, int y) {if (x > y) return x; else return y;} } In una dichiarazione o definizione, x, y sono detti parametri formali sono propri della funzione I parametri formali x, y vengono usati per passare alla funzione le informazioni di cui ha bisogno per il calcolo richiesto.
I parametri di una chiamata di funzione sono detti “attuali” int main () { int n = 7, m = 14, k; k = max(n, m); cout<< " il massimo tra " << n << " e " << m << " vale " << k << endl; } } In una chiamata, n, m sono detti parametri attuali. Sono parametri propri del main Una chiamata copia i valori parametri attuali n, m del main nei parametri formali x, y della funzione, che riceve così tutte le informazioni di cui ha bisogno per il calcolo. La chiamata di una funzione sta nel main o dentro un’altra funzione e non richiede di ripetere i tipi dei parametri
Chiamate di funzione in dettaglio La chiamata di funzione k = max(n, m); crea gli indirizzi di memoria per i parametri formali x,y e assegna loro i valori dei parametri attuali n,m n x 7 7 7 max(int x, int y) m 14 y 14 14 main() Return value k 14 14 14 Alla fine della chiamata gli indirizzi di x, y vengono riutilizzati per altre variabili. Il valore di ritorno di max(n,m) viene assegnato a k. 5-Funzioni
Funzioni: l’istruzione return Una funzione normalmente restituisce un valore attraverso l’istruzione return, che compare nel corpo della definizione della funzione, come segue: return <espressione>; double cube (double x) { return x*x*x; } Il tipo dell’espressione x*x*x argomento del return deve coincidere con il tipo del valore di ritorno (in questo caso devono essere entrambi double). 5-Funzioni
Attenti a non perdere il valore di una funzione Se ci dimentichiamo di salvare il valore di ritorno di una funzione lo perdiamo. Infatti il valore restituito da una funzione di solito sta in un registro della CPU, e può essere salvato solo assegnandolo ad una variabile, per esempio scrivendo: k = max(n, m); In questo caso, il massimo tra i valori di n ed m è ora il nuovo valore di k. Se invece scrivo soltanto: max(n,m); allora il valore di max(n,m) viene perso. 5-Funzioni
Le funzioni si possono usare dentro espressioni Una funzione di tipo T definisce un’espressione di tipo T. Quindi una funzione di tipo T può essere usata in combinazione con qualunque funzione definita o di libreria, e con qualunque operazione aritmetica, per costruire nuove espressioni. Un esempio: 7 + max(3, max(5, m*2) ); In una chiamata di funzione, il parametro attuale di max può anche essere un’espressione come max(5, m*2) che contiene max 5-Funzioni
Le funzioni con tipo void non indicano un valore Esistono funzioni C++ che non restituiscono un valore, ma si limitano a svolgere un’azione (ad esempio la stampa di un valore). Non hanno nessun corrispondente tra le funzioni matematiche. Se una funzione svolge un’azione e non restituisce alcun valore viene definita con il tipo void (vuoto): Ciao(); e Pi(); sono comandi di stampa, non indicano nessun valore. È un errore scrivere cout<<Ciao(); oppure Pi()+1; Non c’è un valore di ritorno di Ciao() da stampare, né un valore di Pi() a cui aggiungere 1. void Ciao () {cout << "ciao! " << endl;} void Pi () {cout << 3.14159265 << endl;}
Funzioni: errori frequenti Il meccanismo di comunicazione tra una funzione e il resto del programma non è semplice, ma deve venir capito a fondo, altrimenti rischiamo errori come i seguenti: Usare una funzione senza valore di ritorno per indicare un valore (abbiamo appena visto un esempio); Leggere da tastiera il valore di un parametro formale. Dimenticarci di salvare il valore di ritorno di una funzione. Nei prossimi lucidi spieghiamo gli errori 2 e 3.
È un errore leggere da tastiera il valore di un parametro formale La comunicazione tra una funzione e il resto del programma avviene quando una chiamata assegna i parametri attuali ai parametri formali. È un errore assegnare noi stessi i parametri formali, per esempio è un errore usare l’istruzione cin >> … come segue: int sq(int n){cout<<"dammi un valore per n: ";cin>>n; return n*n; } // sq e’ scritta in modo errato Infatti quando scriviamo sq(3), il valore 3 passato ad sq attraverso il parametro formale n viene sovrascritto da un altro valore, per es. 10, inserito da noi. Così sq(3) non vale 3*3 come dovrebbe ma 10*10. 5-Funzioni
È un errore dimenticare di salvare il valore di una funzione Abbiamo già visto che se dimentichiamo di salvare il valore di ritorno di una funzione lo perdiamo: int f(int n){ ... return ... } int main() {int num; cin >> num; f(num);} /* il valore restituito da f(num) viene si’ calcolato, ma non viene assegnato a una variabile, quindi va perso. */ 5-Funzioni
3. Variabili locali All’interno del corpo di una funzione può essere utile avere variabili che esistano solo durante l’esecuzione, per evitare di sovrascrivere variabili già in uso. Queste variabili si dicono “variabili locali”: sono proprie alla funzione come i parametri formali, e come essi nascono (nel senso che ricevono un indirizzo di memoria) e muoiono (nel senso che il loro indirizzo viene riciclato) ad ogni chiamata della funzione. int f(int n) { int r, s; ... } Ambito di visibilità delle variabili locali r ed s e del parametro formale n: queste variabili r,s,n esistono solo qui 5-Funzioni
Quali variabili dobbiamo definire come “locali”? Più variabili possibile! Per rendere indipendenti sottoprogramma e programma, dobbiamo definire come variabile locale di una funzione qualunque variabile contenga un risultato intermedio delle funzione, come un contatore o un accumulatore. Inseriamo invece tra i parametri della funzione solo le variabili indispensabili alla funzione. SI: indispensabile NO: contatore NO: accumulatore int fattoriale(int n, int i, int prod) { for(i=2,prod=1;i<=n;++i)prod=prod*i return prod;} 5-Funzioni
Cosa succede se inseriamo una variabile locale tra i parametri? Il programma diventa inutilmente complicato, illeggibile e a rischio di errore. Nell’esempio del fattoriale, se dichiariamo una funzione fattoriale(n,i,prod), per calcolare il fattoriale di 3 dobbiamo scrivere fatt(3,2,1), fornendo non solo il 3, ma anche i valori iniziali i=2 e prod=1. int fattoriale(int n) {int i; int prod; for(i=2,prod=1;i<=n;++i)prod=prod*i; return prod;} SI: indispensabile Contatore locale Accumul. locale 5-Funzioni
A cosa servono le variabili locali? Riassumendo, le variabili locali di una funzione: Sono visibili solo dalle istruzioni nel corpo della funzione. Esistono solo finché la funzione è in esecuzione, quindi spariscono. Lo spazio di memoria che esse occupano viene indicato come “disponibile” e viene assegnato ad altre variabili. Questa operazione viene detta “riciclo” della memoria. Evitano sovrascritture accidentali di variabili con lo stesso nome in altre funzioni o nel main, e nascondono i passi di calcolo della funzione all’interno della funzione stessa. 5-Funzioni
Variabili locali e Stack di sistema (cenni) Per capire meglio come funzionano le variabili locali, vediamo come avviene la creazione e il “riciclo” delle variabili locali. Il sistema gestisce un gruppo di bytes consecutivi detto stack (pila). Ogni volta che dichiariamo una variabile nel main o nella funzione appena chiamata, aggiungiamo alla pila uno spazio di memoria con il valore della variabile. Parte della pila con le variabili generate da una chiamata a una funzione f x 1200CF (indirizzo esadec.) z 1205B1A Parte della pila con le variabili del main() x 06A231 y 105BA3 5-Funzioni
La ricerca di una variabile locale nello Stack di sistema double f(double z) { float x; …} La sola x visibile durante l’esecuzione di f(…) e’ quella nel riquadro più in alto, la x di f int main() { double x, y; … f(x); … } Per cercare il valore di x, leggiamo dall’alto in basso, fermandoci la prima volta che incontriamo x La x del main() non e’ visibile durante l’esecuzione di f(…), ma continua ad esistere x 1200CF (x di f) z 1205B1A x 06A231 (x del main) y 105BA3 5-Funzioni
Parametri e variabili locali: un errore tipico Per errore usiamo due volte lo stesso nome x in f: cosa succede? double f(double x) { float x; …} int main() {double x, y; … f(x); …} Per saperlo, leggiamo lo stack di sistema “dall’alto al basso”, fermandoci alla prima x che troviamo La x variabile locale, dichiarata per ultima, viene trovata per prima e nasconde (in inglese: “shadows”) il parametro formale x x 1200CF x 1205B1A x 06A231 y 105BA3
Variabili locali nei Blocchi La dichiarazione delle variabili locali può anche essere annidata in blocchi delimitati da { } : int f(int n) { int s = 0; { int r, s = 1; /* la seconda s, definita in questo blocco, “fa ombra” alla prima locale alla f */ cout << s; }; cout << r; /* ERRORE: fuori dal blocco in cui è definita, r non esiste più! E se anche esistesse, sarebbe un’altra r … */ cout << s; /* s vale ancora 0, la s che ha preso valore 1 e’ un’altra s */ } 5-Funzioni
Errori frequenti int f(int n) { int s; ... s = ...} int main() { int m = f(7); int p = s; /* ERRORE: fuori dalla funzione f in cui è definita, s non esiste più! E se anche esistesse, sarebbe un’altra s … */ ... } Come il passaggio dei parametri, anche la visibilità è un meccanismo importante nell’uso delle funzioni, e va capita a fondo, altrimenti rischiamo errori come questo: 5-Funzioni
Variabili globali Tutte le variabili sin qui viste sono parametri oppure variabili locali: alcune sono locali al main(), ma in C++ anche il main() è una funzione. Le variabili definite fuori dalle funzioni sono visibili ovunque (tranne in blocchi che abbiano variabili locali omonime) e sono persistenti per tutta l’esecuzione del programma. Sono dette variabili globali. Per esempio, costanti e parametri del programma dovrebbero essere variabili globali. #include <iostream> double pi = 3.14159265; /* pi esisterà per tutto il programma */ 5-Funzioni
Variabili globali: evitare un uso eccessivo Le variabili globali servono per violare le limitazioni di visibilità tipiche delle variabili locali, limitazioni a volte troppo restrittive. L’uso delle variabili globali deve essere moderato, perchè può causare riscritture accidentali di variabili con lo stesso nome, e perchè produce programmi poco leggibili, come nel seguente esempio: double k=0.0; // k esiste per tutto il programma void C(double x) { … k = sqrt(x);… … } int main(){C(25); cout << k;} /* eseguire C(25) assegna k=5.0, ma leggendo la sola riga: C(25); questo non è affatto evidente!! */ 5-Funzioni
5. Passaggio per riferimento Il passaggio dei parametri è detto per valore perche’ comporta una copia del parametro attuale in quello formale. Questo è un difetto quando vogliamo usare una funzione per produrre una modifica permanente: scambia dovrebbe scambiare i suoi argomenti ma agisce solo sulle variabili temporanee x, y void scambia (int x, int y) {int temp = x; x = y; y = temp;} int main() { int n = 45, m = 0; scambia(n, m); cout << n << " \t " << m;} /* i parametri formali x,y, copie di n,m, sono stati scambiati, ma i parametri attuali n,m no!!*/ 5-Funzioni
Passaggio per riferimento In C++ si può passare l’indirizzo di parametro attuale x definendo il parametro formale della funzione come &x. &x denota l’indirizzo di x. Questo passaggio di parametri e’ detto per riferimento e ed è usato per violare le limitazioni di visibilità tipiche dei parametri di una funzione, e per modificare il valore originale dell’argomento x. void scambia (int & x, int & y) { int temp = x; x = y; y = temp; } indirizzo x indirizzo y 5-Funzioni
Passaggio per riferimento scambia() main() n temp x 45 45 m y x ed y in scambia() assumono lo stesso indirizzo di n, m nel main(). x e n sono sinonimi, cioè sono la “stessa” variabile con due nomi diversi (condividono l’indirizzo di memoria). Questo accorgimento consente a scambia() di modificare le x, y originali. 5-Funzioni
Usi del passaggio per riferimento Anche il passaggio per riferimento puo’ creare errori difficili da scoprire: viola le limitazioni di visibilità e consente riscritture accidentali di variabili, quindi va usato con moderazione. Il passaggio per riferimento può essere utile quando si deve restituire più di un valore, per es., q=quoziente e r=resto di n/m. In tal caso si passano gli indirizzi delle variabili q, r in cui vogliamo che siano scritti i risultati. Le variabili q, r originarie vengono modificate. void Div (int n, int m, int & q, int & r) { r = n%m; // calcola quoziente e resto n:m q = n/m; } 5-Funzioni
Usi del passaggio per riferimento Nell’esempio seguente, la chiamata a Div modifica i valori di quoziente e resto: void Div (int n, int m, int & q, int & r) // calcola quoziente e resto di n div. m { … } int main() {int resto=0, quoziente=0; Div(20, 3, quoziente, resto); cout<<quoziente<<"\t"<<resto<<endl;} 5-Funzioni
Errori frequenti Non possiamo passare per riferimento una costante o una espressione. Un argomento passato per riferimento deve avere un indirizzo di memoria, dunque, per quanto ne sappiamo fin qui, può solo essere una variabile: int main () { scambia(7, 5+9); } // ERRORE; né 7 né 5+9 // hanno un indirizzo di memoria 5-Funzioni
6. Come si descrive una funzione? In altre parole, “che cosa” calcola una funzione? La risposta viene chiamata una specifica dell’algoritmo, ed è fatta di due parti: una pre-condizione e una post-condizione Vorrei calcolare valori con la proprietà Sig. Utente Sig. Funzione Pre-condizione Post-condizione Posso farlo purché i dati soddisfino 5-Funzioni
Pre e Post condizioni La pre-condizione è una richiesta, ossia un’ipotesi sull’ingresso (spetta a chi fornisce i dati controllarla) La post-condizione è una proprietà dell’uscita di una funzione, che si deve garantire che valga dopo l’esecuzione. Quando inseriamo nella funzione dei dati che non rispettano la precondizione, otteniamo delle risposte imprevedibili. Il calcolo della funzione può anche continuare per sempre, o produrre un “crash”. 5-Funzioni
Esempi di Pre e Post condizioni int MCD (int n, int m) // pre: n, m >=0 e non entrambi nulli // post: MCD restituisce il massimo comun // divisore tra n ed m void Div (int n, int m, int & q, int & r) // pre: m != 0 // post: q quoziente, r resto della div. n:m 5-Funzioni
Riepilogo I sottoprogrammi sono parti di un programma la cui esecuzione può essere ripetuta in punti diversi del programma principale, o di altri sottoprogrammi. Le funzioni sono sottoprogrammmi con parametri propri, parametri che esistono solo per la durata di una esecuzione della funzione. Le funzioni compaiono in un programma in tre modi diversi: in una dichiarazione, in una definizione e in una o più chiamate. Le funzioni possono avere variabili locali. Le funzioni si descrivono con una specifica, fatta di pre- e post-condizioni. 5-Funzioni
Riepilogo Le funzioni possono restituire un valore, oppure no (in tal caso sono di tipo void) I parametri sono passati per valore oppure per riferimento Le variabili locali oscurano sia le variabili globali che i parametri Se una funzione restituisce un valore, allora la funzione può essere usata in un’espressione per indicare il valore da essa restituito. Altrimenti no. 5-Funzioni