Tutorial
9
Scritto da Roberto Navigli (roberto.navigli@iol.it)
Game Programming Italia: www.gpi.eden.it
Funzioni
Per
via della loro grande utilità, le funzioni
sono state introdotte sin dall'inizio. È bene
però specificare la sintassi con cui esse possono
essere definite.
Specificare una funzione significa dire come svolgere
una determinata operazione. Consideriamo la funzione
come una scatola nera. La scatola ha alcune fessure
attraverso le quali è possibile inserire determinate
informazioni necessarie a portare a termine il compito
che le viene affidato e una sola fessura per restituire
il risultato di tale operazione. Ad esempio, una scatola
di questo genere può essere la macchinetta
del caffè oppure un distributore di biglietti
dei treni. Esse prendono in ingresso del denaro e
restituiscono, rispettivamente, una tazzina di caffè
e un biglietto del treno. Questo è proprio
quello che fanno le funzioni: prendono in input degli
argomenti e restituiscono in output un risultato.
È
bene ora distinguere due concetti fondamentali:
- la dichiarazione di funzione
- la definizione di funzione
La
prima è semplicemente una specifica degli argomenti
che la funzione prende in input e di ciò che
essa restituisce in output. Ad esempio, la dichiarazione
della funzione "macina caffè" - scritta
in italiano - potrebbe essere la seguente:
Funzione:
macina caffè. Input: caffè. Output:
caffè macinato.
Ciò
equivale a una dichiarazione di funzione in C, così
come in C++. Essa avviene con la seguente sintassi:
tipo_output
nome_funzione(tipo1_input,
, tipon_input);
dove
tipo_output è il tipo di oggetto restituito,
nome_funzione è il nome della funzione e dove
tra parentesi tonde si specificano gli n tipi di oggetti
che la funzione prende in input. Per tornare all'esempio
di prima, si avrebbe:
CaffèMacinato
macina_caffè(Caffè);
macina_caffè
è il nome della funzione, CaffèMacinato
è l'output della funzione e Caffè è
l'input della stessa.
Un esempio più concreto potrebbe essere una
funzione che calcola una determinata imposta:
int
calcola_imposta(int);
essa
prende in input un valore intero che rappresenta l'imponibile
e restituisce un altro valore intero, l'imposta da
pagare.
Finora quindi si è parlato solo di dichiarazione
di una funzione in C. Ciò significa che si
comunica al compilatore come è fatta la funzione-scatola
nera. Da quel momento in poi, è possibile far
uso della funzione in qualsiasi punto del codice,
purché si passino in input il numero richiesto
di argomenti e del tipo specificato nella dichiarazione.
Ciò ovviamente non può essere sufficiente,
perché il compilatore non sa come macinare
il caffè o calcolare le imposte. È quindi
necessario dare una definizione delle funzioni che
si dichiarano. A tale scopo è sufficiente utilizzare
la medesima sintassi della dichiarazione, eliminare
il punto e virgola ed inserire un blocco di istruzioni
(racchiuse come sempre da parentesi graffe). Ad esempio:
int
calcola_imposta(int imponibile)
{
// 20% di imposta
return (imponibile*20)/100;
}
Si
noti che non si specifica più solo il tipo
dell'argomento in input (un intero), ma anche il nome
(imponibile), poiché esso deve poter essere
utilizzato all'interno della funzione per poter restituire
il risultato. Tale restituzione avviene mediante l'istruzione
return. Nell'esempio si restituisce il 20% dell'imponibile
passato in input.
Segue un esempio completo:
#include
<iostream>
using
namespace std;
//
dichiarazione della funzione
int calcola_imposta(int);
void
main()
{
// richiama la funzione calcola_imposta su un milione
cout << calcola_imposta(1000000);
}
//
definizione della funzione
int calcola_imposta(int imponibile)
{
// 20% di imposta
return (imponibile*20)/100;
}
Compilando
il file si ottiene il seguente risultato:
200000
che
è il 20% di un milione. Si noti che la funzione
calcola_imposta viene utilizzata dopo la sua dichiarazione,
ma prima della sua definizione. Ciò è
possibile perché al compilatore è sufficiente
conoscere la forma della "scatola nera"
per poterla utilizzare (così come si fa quando
si utilizza una macchinetta del caffè).
Quando si scrivono programmi semplici è anche
possibile fare a meno della dichiarazione. La definizione
di funzione, infatti, ha anche la funzione di dichiarare
la stessa (poiché contiene tutte le informazioni
necessarie a tale scopo). Ad esempio, il codice precedente
si poteva scrivere come segue:
#include
<iostream>
using
namespace std;
//
definizione della funzione
int calcola_imposta(int imponibile)
{
// 20% di imposta
return (imponibile*20)/100;
}
void
main()
{
// richiama la funzione calcola_imposta su un milione
cout << calcola_imposta(1000000);
}
È
invece errato il seguente:
#include
<iostream>
using
namespace std;
void
main()
{
// richiama la funzione calcola_imposta su un milione
// errore: la funzione non è dichiarata né
definita!!!
cout << calcola_imposta(1000000);
}
//
definizione della funzione
int calcola_imposta(int imponibile)
{
// 20% di imposta
return (imponibile*20)/100;
}
Il
compilatore emette un messaggio di errore perché,
quando incontra la chiamata alla funzione calcola_imposta,
essa non è stata ancora definita, né
tantomeno dichiarata!
Ricorsione e mutua ricorsione
A
volte può tornare utile richiamare la funzione
dall'interno della funzione stessa. Ciò non
pone alcun problema, purché ci si accerti che
la funzione non chiami se stessa infinite volte. Ad
esempio, il programma seguente non termina:
//
fun è una funzione che non prende argomenti
in input
// e non restituisce nulla in output
void fun()
{
// chiama se stessa
fun();
}
void
main()
{
fun();
}
Nel
main si chiama la funzione fun, la quale chiama se
stessa, entrando in un circolo vizioso, poiché
ad ogni successiva chiamata essa chiamerà se
stessa. In genere quando si fa uso della ricorsione
si stabilisce una condizione che ponga fine alla catena
di chiamate in cascata. Ad esempio:
int
fattoriale(int n)
{
if (n > 1) return n*fattoriale(n-1);
else return 1;
}
è
una funzione che calcola il fattoriale di un intero.
Come noto, il fattoriale di n, ovvero n!, è
definito come n*(n-1)*(n-2)*
*1. Quello che fa
la funzione ricorsiva è di moltiplicare il
numero in input per il risultato della stessa funzione
applicata al numero decrementato di 1. Ciò
è possibile perché:
n!
= n*(n-1)!
ovvero,
in C:
fattoriale(n)
== n*fattoriale(n-1)
Spesso
è conveniente fare a meno della ricorsione,
poiché essa rende allo stesso tempo meno leggibile
e meno efficiente il codice. Nonostante ciò,
alcuni problemi possono essere risolti semplicemente
solo per mezzo di tale tecnica. Segue la versione
iterativa della funzione fattoriale (che fa uso del
costrutto for):
int
fattoriale(int n)
{
// copia n nella variabile i
int i = n;
// decrementa n
n--;
// da n-1 fino a 1
for (; n > 0; n--) i *= n;
// restituisce il risultato
return i;
}
Passaggio
di puntatori e passaggio di array
A
volte una funzione può avere l'esigenza di
modificare le variabili che essa riceve in input.
Ad esempio:
#include
<iostream>
using namespace std;
void
azzera_coordinate(int *x, int *y)
{
*x = *y = 0;
}
void
main()
{
int x, y;
// la funzione inserisce le coordinate in x e in y
azzera_coordinate(&x, &y);
// stampa le coordinate
cout << x << "," << y;
}
La
funzione azzera_coordinate ha bisogno di modificare
due variabili. Esse vengono passate per indirizzo,
nel senso che la funzione riceve gli indirizzi delle
celle di memoria in cui dovranno essere contenuti
i valori richiesti. Chiaramente una funzione del genere
ha senso se viene invocata molto spesso su coppie
di interi in diversi punti del proprio programma,
sebbene questo non sia l'unico modo in cui sia possibile
gestire le coppie, come si comprenderà in seguito.
Il
seguente esempio, invece, non è un buon esempio
di programmazione:
void
dammi_la_somma(int a, int b, int *risultato)
{
*risultato = a+b;
}
Infatti
il risultato avrebbe potuto essere restituito direttamente
dalla funzione, senza far uso di puntatori:
int
dammi_la_somma(int a, int b)
{
return a+b;
}
Spesso
i puntatori generano confusione. Infatti è
bene tenere a mente che, ogni qualvolta si passa un
puntatore in input a una funzione, il contenuto della
locazione "puntata" può essere modificato.
Ci
si può chiedere come passa in input un array.
La chiave sta nel fatto che un array non è
altro che una sequenza di elementi dello stesso tipo.
In C (come in C++) è sufficiente quindi passare
un puntatore al primo elemento della sequenza. Ad
esempio:
#include
<iostream>
using namespace std;
int
cerca_massimo(int *sequenza)
{
int max = -1;
for (int k = 0; sequenza[k] != -1; k++)
if (sequenza[k] > max) max = sequenza[k];
return
max;
}
void
main()
{
// array di numeri
int numeri[] = { 1, 5, 3, 7, 0, 10, 9, -1 };
cout << cerca_massimo(numeri);
}
Nell'esempio
la funzione prende in input un puntatore a una sequenza
di elementi interi (definita all'interno del main).
Essa scorre la sequenza fino a trovare l'intero -1
restituendo quindi il massimo dei numeri esaminati.
Un'implementazione più efficiente della funzione
fa uso della cosiddetta aritmetica dei puntatori:
int
cerca_massimo(int *sequenza)
{
int max = -1;
while(*sequenza != -1)
{
if (*sequenza > max) max = *sequenza;
sequenza++;
}
return max;
}
Il
puntatore sequenza viene incrementato in modo tale
da farlo puntare di volta in volta all'elemento successivo,
finché non si incontra l'intero -1.
Argomenti
al main
Anche
la funzione main può avere degli argomenti,
ovvero i parametri che seguono il nome del file eseguibile
se esso viene richiamato da console (sotto DOS e Linux,
ad esempio):
#include
<iostream>
using namespace std;
void
main(int argc, char *argv[])
{
for (int k = 0; k < argc; k++)
// stampa l'argomento k-esimo
cout << argv[k] << endl;
}
Gli
argomenti (fissi) del main sono un intero (contenente
il numero di parametri) e un array di puntatori a
stringhe (i parametri stessi). Ad esempio argv[0]
contiene il nome del file eseguibile, argv[1] contiene
il primo parametro (se esso viene specificato) e così
via.
|