Sezione 2.3
Funzioni (II).


Parametri passati per valore e per riferimento.

Negli esempi di funzioni visti finora i parametri venivano passati per valore. Questo significa che quando viene chiamata una funzione quello che viene passato alla funzione è il valore dei parametri (siano essi delle costanti o delle variabili o delle espressioni). In particolare, se il parametro è una variabile viene passato alla funzione il valore della variabile ma non la variabile stessa. Supponiamo, ad esempio, di richiamare la funzione somma nel modo seguente: 
int x=5, y=3, z;
z = somma ( x , y );
In questo caso viene richiamata la funzione somma passandogli i valori di x ed y , ossia 5 e 3 , ma non le variabili stesse: 
In questo modo, quando la funzione somma è chiamata, i valori delle sue variabili a e b sono 5 e 3 rispettivamente. Una modifica di a o b all'interno della funzione somma non cambia i valori delle variabili x ed y esterne ad essa. Questo perché non sono state passate le variabili x ed y alla funzione somma ma soltanto il loro valore .          

Ci sono però casi in cui vogliamo modificare dall'interno di   una funzione il valore di variabili definite esternamente alla funzione stessa.   A questo scopo possiamo usare dei parametri passati per riferimento, come nella funzione raddoppia dell'esempio seguente:

// passaggio di parametri per riferimento
#include <iostream.h>

void raddoppia (int& a, int& b, int& c)
{
  a*=2;
  b*=2;
  c*=2;
}

int main ()
{
  int x=1, y=3, z=7;
  raddoppiqa (x, y, z);
  cout << "x=" << x << ", y=" << y << ", z=" << z;
  return 0;
}
x=2, y=6, z=14

La prima cosa da notare è che nella dichiarazione di raddoppia il tipo di ciascun parametro è seguito dal carattere e commerciale (&); esso sta ad indicare appunto un passaggio di parametro per riferimento invece dell'usuale passaggio per valore .

Quando passiamo una variabile per riferimento è la variabile stessa che noi passiamo alla funzione e non soltanto il suo valore. Di conseguenza una modifica del valore del parametro all'interno della funzione modifica il valore della variabile passata come parametro.

In altre parole noi abbiamo associato le variabili locali a , b e c (i parametri formali della funzione) alle variabili x , y e z (i parametri attuali passati nella chiamata alla funzione) in modo tale che a diventa sinonimo di x , b sinonimo di y e c sinonimo di z . Ricordando che una variabile è il nome di una zona di memoria in cui può essere memorizzato un valore (il valore della variabile appunto), dire che a e x sono sinonimi significa che essi sono nomi diversi per la stessa zona di memoria. Se a ed x sono sinonimi, una modifica del valore di a ha come conseguenza la modifica del valore registrato nella zona di memoria comune ad a e x e dunque anche il valore di x cambia. 

Ecco perché l'output del nostro programma, che stampa i valori delle tre variabili x, y e z , mostra che i valori di tali tre variabili sono  raddoppiati dopo la chiamata alla funzione raddoppia

Se avessimo dichiarato la funzione raddoppia senza il simbolo e commerciale (&):

void raddoppia (int a, int b, int c)
non avremmo passato le variabili x, y e z ma soltanto i loro valori e quindi il programma avrebbe stampato i valori di x , y e z non modificati. 

Il passaggio di parametri per riferimento permette di scrivere funzioni che calcolano più di un valore. Ad esempio, ecco una funzione che calcola il numero precedente ed il numero successivo del primo parametro che gli viene passato:

// calcolo di piu' di un valore
#include <iostream.h>

void precsucc (int x, int& prec, int& succ)
{
  prec = x-1;
  succ = x+1;
}

int main ()
{
  int x=100, y, z;
  precsucc (x, y, z);
  cout << "Precedente=" << y << ", Successivo=" << z;
  return 0;
}
Precedente=99, Successivo=101

Valori di default per i parametri.

Nella dichiarazione di una funzione si possono specificare dei valori didefault per i parametri. I valori di default vengono usati nel caso in cui tali parametri vengano omessi nella chiamata di funzione. Ad esempio:       

// valori di default per i parametri
#include <iostream.h>

int dividi (int a, int b=2)
{
  int r;
  r=a/b;
  return r;
}

int main ()
{
  cout << dividi (12);
  cout << endl;
  cout << dividi (20,4);
  return 0;
}
6
5

Nel programma precedente ci sono due chiamaste alla funzione dividi. Nella prima:

divide (12)
viene passato un solo argomento mentre la funzione ne richiede due. Siccome il secondo parametro ha valore di default 2 è proprio il valore 2 che viene passato implicitamente come valore del secondo parametro b. Quindi il risultato che si ottiene è 6 (12/2 ). 

Nella seconda chiamata:
dividi (20,4)
vi sono entrambi i parametri, quindi il valore di default 2 non viene usato ma viene passato il valore 4 come valore del secondo parametro b . Quindi il risultato che si ottiene è 5 ( 20/4 ).

Notare che la corrispondenza tra parametri attuali e parametri formali è posizionale e quindi in una chiamata si possono omettere soltanto gli ultimi parametri. Di conseguenza, in una chiamata di funzione con valori di default per i parametri è possibile omettere tutti i parametri da un certo punto in poi ma non ometterne uno intermedio.

Funzioni sovraccaricate.

Due funzioni distinte possono avere lo stesso nome purché la lista degli argomenti sia diversa. Questo significa che possiamo dare lo stesso nome a più di una funzione purché esse abbiano un diverso numero di parametri o almeno un parametro di dipo diverso. Ad esempio:

// funzione sovraccaricata
#include <iostream.h>

int dividi (int a, int b)
{
  return a/b;
}

float dividi (float a, float b)
{
  return a/b;
}

int main ()
{
  int x=5,y=2;
  float n=5.0,m=2.0;
  cout << dividi (x,y);
  cout << "\n";
  cout << dividi (n,m);
  cout << "\n";
  return 0;
}
2
2.5

In questo caso abbiamo definito due funzioni con lo stesso nome dividi ma con parametri di tipo diverso: la prima con due parametri di tipo int, la seconda con due parametri di tipo float. Il compilatore decide quale delle due debba essere chiamata esaminando il tipo dei parametri attuali forniti nella chiamata. 

Nell'esempio le due funzioni hanno lo stesso corpo ma questo non è necessario: funzioni con lo stesso nome possono anche fare cose completamente diverse.  

Ricorrenza.

La ricorrenza (o ricorsività) è la proprietà di una funzione di poter essere richiamata da se' stessa, ossia all'interno del corpo della funzione possono comparire chiamate alla funzione stessa. Questa possibilità risulta particolarmente utile in certe situazioni in cui il valore da calcolare può essere definito per induzione . Ad esempio, il fattoriale di un numero intero n
n! = n * (n-1) * (n-2) * (n-3) ... * 1
si può definire induttivamente nel seguente modo:
il che suggerisce la seguente funzione ricorsiva:  

// calcolo del fattoriale
#include <iostream.h>

long fattoriale (long a)
{
  if (a > 1)
   return a * fattoriale (a-1);
  else
   return 1;
}

int main ()
{
  long n;
  cout << "Dammi un numero: ";
  cin >> n;
  cout << n << "!"<< " = " << fattoriale (n);
  return 0;
}
Dammi un numero:9
9! = 362880

Osserviamo che nella funzione fattoriale viene ricorsivamente effettuata una chiamata alla funzione fattoriale stessa. La chiamata ricorsiva  viene però effettuata soltanto se l'argomento è maggiore di 1 , altrimenti la funzione entrerebbe in un ciclo di ricorsione infinito , venendo richiamata con argomento 0 e quindi con argomento -1 , -2 e così via.

Un'altra osservazione è che la funzione fattoriale ha una limitazione nei valori dell'argomento dovuta al fatto che il fattoriale di un intero cresce molto rapidamente. In pratica, il tipo del risultato (long) non permette di memorizzare fattoriali maggiori di  12!.

Prototipi di funzioni.

Finora abbiamo sempre messo la definizione di una funzione prima della prima occorrenza di una chiamata alla funzione stessa che generalmente appare nella funzione main. Per questa ragione abbiamo dovuto mettere sempre la funzione main alla fine. Se negli esempi precedenti avessimo messo la funzione main per prima avremmo ottenuto una segnalazione di errore. La ragione è che quando viene richiamata una funzione essa deve essere già nota al compilatore.

In realtà il compilatore per poter effettuare una chiamata di funzione ha bisogno di conoscere soltanto il nome della funzione ed il numero e tipo dei suoi parametri (il prototipo della funzione ) mentre non ha alcun bisogno di conoscerne il corpo. Il C++ permette di dichiarare il prototipo di una funzione in modo tale da renderla nota al compilatore e rimandare in seguito la definizione vera e propria della funzione (comprendente anche il corpo). 

La forma di una dichiarazione di prototipo è la seguente :

tipo none ( tipo_parametro1, tipo_parametro2, ...);
ed è simile alla intestazione di una dichiarazione di funzione eccetto: Ad esempio:

// prototipazione

#include <iostream.h>

void dispari (int a);
void pari (int a);
int main ()
{
  int i;
  do {
    cout << "Scrivi un numero: (0 per uscire)";
    cin >> i;
    dispari (i);
  } while (i!=0);
  return 0;
}

void dispari (int a)
{
  if ((a%2)!=0) cout << "Il numero è dispari.\n";
  else pari (a);
}
void pari (int a)
{
  if ((a%2)==0) cout << "Il numero è pari.\n";
  else dispari (a);
}
Scrivi un numero (0 per uscire): 9
Il numero è dispari.
Scrivi un numero (0 per uscire): 6
Il numero è pari.
Scrivi un numero (0 per uscire): 1030
Il numero è pari.
Scrivi un numero (0 per uscire): 0
Il numero è pari.

Questo esempio non ha una grande utilità: chiunque sarebbe in grado di ottenere lo stesso risultato con un programma molto più semplice. Ma lo scopo dell'esempio è illustrare come funziona la prototipazione. Inoltre, in questo caso la prototipazione di almeno una delle due funzioni dispari e pari è indispensabile in quanto le due funzioni si richiamano vicendevolmente. 

All'inizio del programma compaiono i prototipi delle funzioni dispari e pari:               

void dispari (int a);
void pari (int a);
che permettono di usare le due funzioni prima che esse siano completamente definite, ad esempio in main che adesso può essere messo nella posizione più logica, cioè all'inizio del programma.  

Molti programmatori esperti consigliano di prototipare tutte le funzioni. Questo è particolarmente utile quando un programma contiene molte funzioni o le definizioni delle funzioni sono molto lunghe. In tal caso raccogliere tutti i prototipi nello stesso posto all'inizio facilita la ricerca se non si ricorda come devono essere richiamate (numero e tipo dei parametri).  


Precedente:
2-2. Funzioni (I).

indice
Seguente:
3-1. Array.