Lezione 7 – Funzioni
Nel mondo reale i programmi tendono ad essere molto grandi e complicati. Per gestire una simile complessità, sono disponibili un certo numero di tecniche di programmazione note come progettazione top-down. La progettazione top-down è l'arte della decomposizione di un problema complesso in attività di complessità ridotta e più facilmente gestibili.
Queste piccole attività formano la base della scrittura di un insieme di moduli che possono essere collegati fra loro a formare un programma completo. In questo modo si ottengono numerosi vantaggi: codice breve, ricerca degli errori più semplice, possibilità di lavorare in gruppo, riutilizzo del codice, librerie.
Un modulo in C++ è noto come funzione e consiste di un prototipo e di una definizione. Come accennato nella lezione 2, ci sono due regole che bisogna rispettare:
- Il prototipo deve essere definito prima che il programma principale inizi.
- Le definizioni devono essere poste dopo il termine del programma principale.
Dichiarazione di una funzione: il Prototipo
Come accennato il prototipo deve essere posto prima che il programma principale inizi ed è composto da tre elementi (visibili nel disegno sottostante):

- Input: Gli input della funzione sono racchiusi tra parentesi tonde e sono noti come parametri di input, ma in realtà sono delle vere e proprie variabili locali alla funzione (vedi Lezione 2 – main()) e quindi visibili solo ed esclusivamente dalla funzione in questione. Si deve sempre specificare il tipo di ogni parametro uno alla volta, ovvero non è possibile utilizzare la forma abbreviata per parametri che hanno lo stesso tipo (ad es.: int i,j,k; per le funzioni non è ammesso).
- Nome Funzione: Il nome della funzione deve essere unico e non deve coincidere con le parole chiavi del C/C++ (ad es.:
for,switch,…). Inoltre, per il nome vale la stessa regole delle variabili: deve iniziare con una lettera o con '_' e contenere poi solo lettere, numeri o '_', tutto il resto è vietato.
- Output: Viene specificato solo il tipo di dati che la funzione deve restituire: int, float, double, char, struct e puntatori ad uno qualunque dei tipi visti. Quando si parlerà più in dettaglio della OOP si vedrà come una funzione può tornare anche un oggetto.
Definizione della funzione: implementazione
Il corpo di una funzione è esattamente come quello di un normale programma C/C++ ovvero del main() e può includere proprie variabili locali ed istruzioni. Ripetiamo ancora una volta che tutte le variabili definite all'interno delle parentesi graffe del corpo della funzione sono invisibili all'esterno della funzione stessa.
Ora che la funzione è dichiarata e implementata, come si utilizza? L'operazione è semplicissima, basta scrivere il suo nome ed inserire tra le parentesi tonde i diversi valori da assegnare ai rispettivi parametri e terminare la riga con il ';'.
Passiamo ora ad esaminare un primo semplice esempio completo:
#include <iostream.h>
// Prototipo
void Show(int x);
int main()
{
int number;
cout << "Enter a number: ";
cin >> number;
Show(number);
return 0;
}
// Implementazione
void Show(int x)
{
cout.setf(ios::right); //***** - vedi sotto
cout << "The number is: ";
cout.width(6); //***** - vedi sotto
cout << x << endl;
}
Si noti dal definizione del prototipo, il main e l'implementazione della funzione. Si provi al calcolatore il risultato ottenuto cercando anche di capire cosa eseguono le righe commentate con gli asterischi (un buon programmatore deve cimentarsi da subito a capire le istruzioni che non ha mai visto).
Passaggio per Valore e per Riferimento
I due concetti esposti in questo paragrafo sono importantissimi e permettono di risolvere diversi problemi di ottimizzazione della memoria utilizzata e velocità di esecuzione dei programmi.
Quando un valore viene passato ad una funzione come parametro di input, il contenuto di quella variabile viene copiato nella variabile interna dichiarata nell'intestazione della funzione. All'interno della funzione, solo il valore copiato viene manipolato; il valore originale rimane inalterato. L'esempio precedente è tipico di un passaggio di parametro per valore nel quale avrei anche potuto modificare il contenuto della variabile x senza avere influenza sulla variabile number.
Il secondo importante concetto che bisogna capire perfettamente è il passaggio dei parametri per Riferimento. Ricordiamo quanto detto nella lezione 3 a proposito del tipo di dato puntatore: un puntatore è un tipo di dato che contiene l'indirizzo di memoria di un'altra variabile.

Quando una variabile puntatore esterna viene passata come parametro di input a una funzione, il contenuto di quella variabile, costituito da un indirizzo, viene copiato nella variabile puntatore interna dichiarata nell'intestazione della funzione. Poiché un indirizzo punta ad una locazione di memoria all'interno del computer, questo significa che entrambe le variabili puntatore, interna ed esterna, puntano alla stessa posizione. Quest'area di memoria può essere quindi manipolata dall'interno della funzione o dal programma principale. Questa è la cosiddetta memoria condivisa.
Vediamo subito un esempio:
#include <iostream.h>
void Twice(int* x);
int main()
{
int* number; //puntatore ad intero
int Num1 = 77;
number = &Num1; //number prende l'indirizzo della variabile Num1
cout << *number << endl;
Twice(number);
cout << *number << endl;
return 0;
}
void Twice(int* x)
{
*x = *x * 2; //modifica la locaz. di memoria il cui indirizzo è contenuto in x
}
Nel precedente esempio facciamo attenzione a due particolari metodi di scrittura che finora non abbiamo visto:
- &nome_var: questa scrittura va letta come "indirizzo della variabile nome_var". Quindi il risultato di questa espressione è un puntatore che ha lo stesso tipo della variabile nome_var.
- *nome_var: questa scrittura è leggermente più complicata da capire. Prima di tutto la variabile nome_var è di tipo puntatore (a intero, carattere,…). L'asterisco va letto come "contenuto della locazione di memoria", quindi tutta l'espressione si legge così: "contenuto della locazione di memoria puntata da nome_var". Nei casi in cui potrebbe crearsi confusione, è possibile usare le parentesi tonde ovvero scrivere, per l'esempio precedente,
(*x) = (*x) * 2;.
Ricorsione
Una definizione che adoperasse il concetto stesso che intende definire sarebbe considerata circolare e dunque vuota, per le stesse ragioni per cui non accetteremmo come concludente una "dimostrazione" che facesse uso della tesi da stabilire. Tuttavia esistono forme di circolarità che non sono considerate vuote, ma anzi accettate come definizioni e ragionamenti validi: sono quelli in cui si ricorre all'induzione matematica. Come in generale si possono definire induttivamente insiemi, così, in particolare, si può fare per le funzioni:

Possiamo tuttavia calcolare i valori di una funzione definita per ricorsione interpretando la definizione implicita come una regola di calcolo. Questo è quanto avviene nel caso di funzioni ricorsive in C++ (ed in ogni linguaggio che accetti la ricorsione: ad esempio il PASCAL, il C, Java). La funzione fattoriale si può infatti implementare:
int Fattoriale(int n)
{
if(n == 0) return 1;
return n * Fattoriale (n-1);
}
La valutazione di Fattoriale(3) si può descrivere nel seguente modo: poiché 3 ≠ 0 il valore sarà 3*Fattoriale(2); il calcolo di Fattoriale(3) viene allora sospeso, per valutare Fattoriale(2), che a sua volta richiede di calcolare 2*Fattoriale(1) e dunque viene anch'esso sospeso in attesa che la chiamata Fattoriale(1) ritorni un valore. Analogamente Fattoriale(1) richiede di calcolare 1*Fattoriale(0), e viene sospeso per calcolare Fattoriale(0). Finalmente l'argomento è 0, dunque sappiamo esplicitamente che il valore di Fattoriale(0) è 1. Da questo momento si riprendono una dopo l'altra le computazioni delle chiamate sospese: troviamo allora che Fattoriale(1) ritorna 1; che Fattoriale(2) ritorna 2, ed infine che Fattoriale(3) ritorna 6.
Come questo semplice esempio suggerisce, non è agevole eseguire a mente i calcoli che una definizione ricorsiva di una funzione comporta. Al contrario abbiamo un'idea molto più chiara di cosa faccia la versione iterativa della stessa funzione fattoriale:
int Fattoriale_iterativo(int n)
{
int fact=1;
for(int i=2; i<=n; i++) fact *= i;
return fact;
}
Concludiamo questo discorso un pò articolato con una semplice frase che racchiude tutto il senso del paragrafo: una funzione C/C++ è ricorsiva quando all'interno del suo corpo viene richiamata se stessa!

|
categorie: Tecnologia
|
Hello 








