Introduzione alle funzioni

Nella programmazione imperativa il codice è un susseguirsi di istruzioni che vengono eseguite una dopo l'altra e, come abbiamo detto, è un metodo di programmazione disordinato e che porta alla scrittura di codice incomprensibile.
Avere un solo blocco di codice con centinaia e centinaia di istruzioni non è certo il massimo della leggibilità.
Sino ad ora ci siamo limitati a scrivere il nostro codice all'interno della funzione main, ma i linguaggi procedurali come il C ed il C++ offrono la possibilità di suddividere il codice in blocchi, chiamati funzioni.

La funzione main è una funzione particolare, è il punto di collegamento tra il sistema operativo ed il programma.
Tuttavia è assurdo pensare di poter inserite tutto il codice al suo interno, cosa che può andare bene per piccoli esercizi, ma diventa ingestibile quando si tratta di programmi di una certa grandezza.

Oltre alla leggibilità che si ottiene grazie alla possibilità di suddividere il codice in blocchi, le funzioni offrono un'altra importante caratteristica, quella di riuso del codice.

Immaginiamo di avere un programma che calcola l'iva:
int main()
{
risultato = 1200 * 20 / 100;

//codice

risultato = 800 * 20 / 100;

//codice

risultato = 750 * 20 / 100;

//codice

risultato = 1250 * 20 / 100;

//codice

risultato = 2200 * 20 / 100;

//codice

return 0;
}
Come potete vedere l'operazione per calcolare l'iva è ripetuta più volte all'interno dello stesso programma.
Immaginiamo che si ripeta per centinaia e centinaia di volte. Il povero programmatore deve riscrivere ogni volta la stessa operazione.
Non sarebbe bello avere una funzione in cui inserire solo il numero su cui calcolare l'iva, e la funzione fa tutto il resto da sola, senza dover riscrivere ogni volta * 20 / 100.
Inoltre immaginiamo se all'improvviso il governo decide di abbassare l'iva del 2%. Ci toccherebbe modificare a mano ogni linea inserendo 18 al posto di 20.
Naturalmente si tratta solo di un semplice calcolo, ma immaginiamo quanto può essere scomodo avere lo stesso blocco di codice copia-incollato centinaia di volte qua e la per tutto il programma. Oltre alla scomodità di doverlo incollare ogni volta, immaginiamo quanto può essere frustante accorgersi di aver sbagliato qualcosa e dover modificare ogni copia del blocco di codice.

Le funzioni servono proprio ad evitare questi problemi.
Vediamo ora un programma che, dato un valore in ingresso, restituisce in uscita lo stesso valore a cui è stata sottratta l'iva:
#include <iostream>

using namespace std;

float calcolaIva(float varCifra) //FUNZIONE CHE SOTTRAE IVA

{
float iva, risultato; //VARIABILI CHE CONTERRANNO L'IVA DA SOTTRARRE E IL RISULTATO FINALE

iva = varCifra * 20 / 100; //CALCOLA L'IVA SUL VALORE PASSATO ALLA FUNZIONE

risultato = varCifra - iva; //SOTTRAE L'IVA CALCOLATA DAL VALORE INIZIALE

return risultato; //RESTITUISCE IL RISULTATO
}

int main()
{
float var1;

//RICHIAMA LA FUNZIONE, GLI PASSA IL VALORE 1200, LA FUNZIONE CALCOLA L'IVA E RESTITUISCE IL RISULTATO

var1 = calcolaIva(1200);

cout << var1; //var1 CONTIENE IL RISULTATO RESTITUITO DALLA FUNZIONE calcolaIva()

return 0;
}
Nella prima parte del programma definiamo la funzione calcolaIva().
Vediamo da cos'è composta questa funzione:

float calcolaIva(float varCifra)

La prima riga è formata dal nome della funzione, che abbiamo deciso di chiamare calcolaIva, seguita da delle parentesi tonde.

Le parentesi tonde contengono la dichiarazione di una variabile di tipo float, che noi abbiamo chiamato varCifra.
Questa variabile serve per passare l'input alla funzione. Nel momento in cui, all'interno del programma, richiamiamo la funzione in questo modo:

calcolaIva(1200);

la variabile varCifra assume il valore 1200.

All'interno del blocco di codice delimitato dalle parentesi graffe siamo così in grado di ricevere il valore 1200 grazie alla variabile varCifra:
    iva = varCifra * 20 / 100; //ESEGUE: 1200 * 20 / 100 = 240

risultato = varCifra - iva; //ESEGUE: 1200 - 240 = 960
Infine, nell'ultima riga di codice all'interno del blocco, restituiamo il risultato:
    return risultato; //RESTITUISCE IL VALORE 960
Avrete notato che il nome della funzione è preceduto dal tipo float.
Questo sta ad indicare il tipo di ritorno, e cioé il tipo di valore restituito dalla funzione.
Dal momento che la variabile risultato, che contiene il valore finale da restituire, è di tipo float, la nostra funzione deve essere definita anch'essa di tipo float:

float calcolaIva(float varCifra) //LA FUNZIONE E' PRECEDUTA DAL TIPO float
{
float iva, risultato; //LA VARIABILE risultato E' DI TIPO float

//...CODICE...

return risultato; //LA VARIABILE RESTITUITA E' DI TIPO float
}
Poiché la variabile risultato, che è la variabile restituita tramite return, è di tipo float, è necessario far precedere la funzione dal tipo float.

Cosa succede invece nella funzione main()?
Non facciamo altro che richiamare la funzione, passargli un valore e ricevere un risultato.

La forma generale di una funzione è questa:
tipo_restituito(valori_di_input)
{
//...codice...

return valore_di_output;
}
Dove tipo_restituito dev'essere dello stesso tipo di valore_di_output
.

I valori ricevuti in input dalla funzione, e cioè quelli passati all'interno delle parentesi tonde, sono detti parametri della funzione.
Il valore di output invece è detto valore di ritorno.
Infine il blocco di codice della funzione racchiuso tra parentesi quadre è detto corpo della funzione.

Per richiamare una funzione basta utilizzare il suo nome, e passargli i dati all'interno delle parentesi tonde.
Ad esempio in una funzione che calcola la somma basterà passare alla funzione i due numeri da sommare:

varRisultato = calcolaSomma(27, 33);

La variabile sommerà i due numeri e restituirà il risultato, che verrà memorizzato in varRisultato.
Naturalmente i valori passati alla funzione devono essere coerenti con il tipo dei parametri della funzione:
int sommaInteri(int primoNumero, int secondoNumero)
{
int risultatoSomma = primoNumero + secondoNumero;

return risultatoSomma;
}

int main()
{
int varRisultato = sommaInteri(23.7, 55.3); //ERRATO!

return 0;
}
Nel nostro esempio abbiamo una funzione che accetta in ingresso due valori int, li somma e restituisce in uscita il risultato della somma.
Nel richiamare la funzione all'interno di main() però, noi le passiamo due numeri decimali (23.7 e 55.3), mentre i due parametri della funzione sono di tipo int.

Non siamo limitati a passare dei valori diretti ad una funzione, avremmo anche potuto passare delle variabili:

int varRisultato, numeroUno, numeroDue;

numeroUno = 23;
numeroDue = 55;

varRisultato = calcolaSomma(numeroUno, numeroDue);

In questo modo passeremo alla funzione il valore contenuto nelle due variabili.
Anche qui l'importante è che le variabili passate alla funzione siano dello stesso tipo dei parametri della funzione stessa.
In questo esempio sono entrambi di tipo int, quindi va bene.

Lo stesso vale per il valore di ritorno, che non dev'essere per forza una variabile, ma può anche essere una costante.
Vediamo un esempio:
bool controllaNumero(int ilNumero)
{
if(ilNumero >= 0) //SE IL NUMERO PASSATO ALLA FUNZIONE E' POSITIVO
{
return true; //RESTITUISCI TRUE
}
else //ALTRIMENTI
{
return false; //RESTITUISCI FALSE
}
}
La funzione che abbiamo scritto riceve in input un numero intero, e tramite l'istruzione if controlla se è maggiore o minore di zero:
se è maggiore o uguale a zero restituisce il valore boleano true;
se è minore di zero restituisce il valore boleano false;

Come potete vedere non abbiamo restituito una variabile ma un valore diretto, in questo caso di tipo bool (notare che il nome della funzione è preceduto dalla parola bool), ma potrebbe essere ad esempio un numero intero o una stringa.

Questa funzione non fa altro che calcolare se il numero passatole è positivo o negativo, e restituisce true nel caso sia positivo e false nel caso sia negativo.

Una funzione che restituisce valori boleani può essere utilizzata ad esempio in questo modo (inserite il codice seguente all'interno di main):
int numeroInserito;

cout << "inserire un numero: ";
cin >> numeroInserito;

if( controllaNumero(numeroInserito) == true)
{
cout << "è positivo ";
}
else
{
cout << "è negativo ";
}
Vediamo cosa succede in questo programma:

- richiamiamo la funzione controllaNumero() all'interno dell'if;

- passiamo alla funzione controllaNumero() la variabile numeroInserito, che contiene il numero digitato dall'utente;

- la funzione controllaNumero() controlla se numeroInserito è maggiore o uguale a 0;

- se lo è, la funzione restituisce true, altrimenti restituisce false;

- a questo punto l'if all'interno del quale abbiamo richiamato la funzione, non fa altro che ricevere il valore boleano restituito dalla funzione stessa e controllare se è uguale a true;

- se è uguale a true stampa "è positivo", altrimenti stampa "è negativo";

In altre parole la riga:
if( controllaNumero(numeroInserito) == true)
Può essere letta come:

"SE il valore restituito da controllaNumero E' UGUALE A true"

Quando si lavora con i valori boleani è possibile estromettere la parte "== true", basta scrivere semplicemente la funzione all'interno dell'if, e se la funzione restituisce true viene eseguito il blocco di codice appartenente all'if:

if( controllaNumero(numeroInserito) )

Oppure se vogliamo che il blocco di codice dell'if sia eseguito se la funzione restituisce false, basta utilizzare l'operatore logico di negazione:

if( !controllaNumero(numeroInserito) )

Basta far precedere la funzione dall'operatore !, e il blocco di codice dell'if verrà eseguito solo se il valore restituito dalla funzione è negativo.

Un'esempio in cui una funzione del genere può essere utile, per tornare al nostro caro esempio del videogioco, è in un sistema di riconoscimento delle collisioni.

Una delle parti più importanti di un videogioco è l'interazione tra i vari oggetti che lo compongono, e per far sì che ciò avvenga è necessaria una funzione che riconosce quando due oggetti si toccano.
All'inizio potremmo pensare di fare una funzione del genere:
void collisione(Sprite &elemento1, Sprite &elemento2)
{
if(elemento1.posizione == elemento2.posizione)
{
elemento1.movimento = 0;
elemento2.movimento = 0;
}
}
La prima cosa che salta all'occhio è che la funzione è preceduta dalla parola void.
La parola void sta ad indicare che la funzione non restituisce nessun risultato.

Infatti la nostra ipotetica funzione si limita a ricevere in ingresso due variabili del tipo Sprite (gli sprite in un videogioco sono i vari elementi che lo compongono, personaggi, blocchi ecc..), dove Sprite è una struttura creata da noi, che è composta da due membri: "posizione", che tiene conto della posizione in cui si trova un determinato elemento, e "movimento" che, se impostato a 0 fa sì che l'elemento si fermi.

La funzione non fa altro che che controllare la posizione dei elementi passetigli, e se la posizione dei due elementi coincide (==) imposta il loro movimento a zero.
Da un punto di vista pratico vuol dire che quando due elementi di gioco si incontrano la funzione li ferma in modo che l'uno non attraversi l'altro.

Per ora ignorate pure le & che precedono i due parametri della funzione, di questo parleremo in un altro post.

La funzione va bene, ma c'è un piccolo problema: non tutti gli elementi del gioco reagiscono allo stesso modo durante una collisione.
I proiettili esplodono, i nemici svaniscono, i blocchi con le monete si rompono, il nostro personaggio si ferma se incontra un muro, mentre perde punti vita se incontra un nemico, addirittura ci sono alcuni nemici che sono dei fantasmini che passano attraverso le pareti.

Perciò decidiamo che la nostra funzione si limiterà a controllare se avviene un contatto tra due elementi di gioco: se avviene restituisce true; se non avviene restituisce false.

In questo modo la stessa funzione potrà essere utilizzata all'interno delle altre funzioni che gestiscono il comportamento di ogni diverso elemento di gioco, ed ognuna di queste funzioni deciderà di reagire in modo diverso alla collisione.

Ad esempio, ci stiamo occupando della funzione che gestisce il movimento del nostro personaggio.
L'utente preme un tasto e, sinché il personaggio non va a contatto con un muro può andare avanti tranquillo; mentre nel momento in cui incontra un muro si ferma.
La nostra funzione si dovrà preoccupare solo di controllare il movimento, per vedere se c'è una collisione si limiterà a chiamare la funzione collisione(), e sarà lei a controllarlo.
La nostra funzione movimento() invece si limiterà a raccogliere il valore restituito dalla funzione collisione(), e sinché questo valore è true il nostro personaggio si può muovere:
void movimento()
{
while( !collisione(varPersonaggio, varMuro) ) //sinché non c'è una collisione
{
varPersonaggio.posizione++; //incrementa la posizione di uno
}
}
La nostra funzione movimento non contiene parametri perché non ha bisogno di dati in ingresso, dal momento che si limita a far muovere il nostro personaggio.
Non ha neanche valori di ritorno, infatti la funzione è void.

Ricordate come funzione il ciclo while?
Esegue l'istruzione all'interno del proprio blocco, sinché si verifica la condizione all'interno delle sue parentesi tonde.

In questo caso all'interno delle parentesi tonde viene richiamata la funzione collisione(), e quindi il ciclo while dovrebbe continuare sinché la funzione collisione() restituisce true.
Tuttavia l'operatore ! che precede la funzione nega il suo risultato, e di fatto il ciclo while continua sinché il valore restituito dalla funzione collisione() è uguale a false.

Il ciclo while contiene all'interno del blocco una linea di codice che incrementa la posizione del personaggio, ed esegue questa linea di codice sinché la funzione collisione() restituisce false, e cioé sinché non c'è una collisione tra il nostro personaggio e un muro.
Nel momento in cui avviene una collisione, la funzione restituisce true, ed il ciclo while si ferma, arrestando il movimento del nostro personaggio.

Come potete vedere le funzioni sono utilissime, perché possono essere richiamate dove vogliamo.
Ad esempio la nostra funzione collisione() può essere richiamata dalla funzione che gestisce il comportamento dei proiettili, e quando la funzione collisione() restituisce true, la funzione proiettili() fa esplodere il proiettile;
allo stesso modo la funzione nemici() può chiamare la funzione collisione() e far bloccare i nemici quando questi incontrano un muro, mentre in un'altra sezione chiamare di nuovo la funzione collisione() e questa volta controllare se a collidere con i nemici è un nostro proiettile: se è così, e cioé se la funzione collisione() restituisce true, il nemico muore.

In questo modo abbiamo scritto una sola volta il codice che gestisce le collisioni, e possiamo richiamarlo infinite volte all'interno del nostro gioco, e addirittura utilizzarlo per altri progetti.
Senza le funzioni avremmo dovuto riscrivere il codice che controlla le collisioni decine e decine di volte all'interno del nostro programma.

Naturalmente il nostro è un esempio semplicistico, nei veri videogiochi una funzione per il controllo delle collisioni è molto più complessa, così come lo sono le varie funzioni per il controllo del movimento di un personaggio e così via.

L'importante è aver capito come si definisce una funzione, da che parti è composta, e come è possibile richiamarla all'interno del nostro codice.

Nessun commento:

Posta un commento