Capitolo 9 - Puntatori a funzioni

                                              di Roberto Porro

In questa parte del corso tratteremo l'argomento dei puntatori
a funzione.
Anche le funzioni, come tutti i tipi del C (con la sola esclu-
sione del tipo register), vengono associate ad un indirizzo in
memoria e, tramite tale indirizzo, possono venir utilizzate.
Ricordiamo che le funzioni C sono contraddistinte dalla coppia
di parentesi tonde () che segue il nome della funzione stessa.
Una volta definita la funzione ogni successiva identificazione
del suo nome senza parentesi fa riferimento al suo indirizzo.
Vediamo un esempio:

main()
{
  int funz();

  printf( "La funzione funz e' all'indirizzo %d\n", funz );
  funz();
}

funz()
{
  /* codice */
}

Questo  programma stampa l'indirizzo in memoria della funzione
funz.

Ora  che siamo a conoscenza dell'indirizzo di funz siamo anche
in grado di chiamare funz tramite il suo indirizzo.
Vediamo praticamente come si fa.

main()
{
  int funz();
  int( *funzpt )();

  printf( "La funzione funz e' all'indirizzo %d\n", funz );
  funzpt = funz;

  printf( "funzpt fa riferimento a %d\n", funzpt );
  funz();
  (*funzpt)();
}

funz()
{
  /* codice */
}

La novita' in questo programma e' che abbiamo scritto 

  int ( *funzpt )();

ovvero il nome di una variabile racchiuso tra parentesi.
Se pensiamo  ai puntatori usati fino  ad ora possiamo chiarire
il concetto:

  int *puntatore;

segnala  al compilatore di  restituire un valore  di tipo int,
ovvero contiene l'indirizzo di una variabile intera, ovvero e'
un puntatore ad intero.
Analogamente

  int ( *funzpt )();

segnala che  la (*funzpt)() ritorna un  valore di tipo intero;
se  poi funzpt contiene l'indirizzo  della funzione funz, come
e' stato dichiarato quando abbiamo scritto

  funzpt = funz;

allora la *funzpt  e' l'inizio  della funzione  stessa (quello
che   viene  detto  entry  point  della  funzione)  e  percio'
restituisce un tipo int dopo aver eseguito la funzione.

Il  fatto di  aver racchiuso  il nome  in parentesi  sta nella
necessita' di  attribuirgli  il  significato  di  puntatore  a
funzione di tipo intero.
Se avessimo utilizzato la notazione

  int *funzpt();

le  parentesi relative agli eventuali parametri della funzione
avrebbero avuto la precedenza sull'operatore *, il che avrebbe
causato la  dichiarazione  di  una  funzione  che  ritorna  un
puntatore ad una VARIABILE di tipo int e non ad una FUNZIONE.

Una funzione chiamata tramite puntatore non deve obbligatoria-
mente restituire un valore intero, ma qualunque tipo.

Esempio.

main()
{
  char *funz();
  char *( *funzpt )();

  funzpt = funz;
  printf( "%s", funz() );
  printf( "%s", (*funzpt)());
}

char *funz()
{
  return( "Ho eseguito funz()\n" );
}

La domanda legittima che ci si  pone  a  questo  punto  e'  Ma
perche' complicarsi la vita con questi puntatori?

Effettivamente se ne  puo' fare  a meno,  ma il  loro utilizzo
puo' servire a semplificare la struttura dei programmi.

Vediamo un esempio.

Supponiamo di  avere un programma  che deve fare  una serie di
operazioni in base ad una valore che viene passato di volta in
volta diverso e supponiamo  ancora  che  questo  valore  venga
fornito  o da  riga di  comando o  dalla selezione  di bottoni
presenti nell'interfaccia del nostro programma a finestre.
Per esempio vogliamo  raddoppiare  il  primo  valore  passato,
dividere per tre il secondo ed elevare al quadrato il terzo.
In una  successiva elaborazione i  parametri potrebbero essere
in numero  maggiore o  minore o  la sequenza  delle operazioni
potrebbe essere diversa.

Il modo per passare i dati potrebbe essere il seguente:

calcola doppio 2 terzo 12 cubo 6
calcola cubo 3 doppio 4
calcola terzo 9 terzo 24 doppio 78 cubo 16

Il programma calcola puo' essere scritto cosi':

...
main( argc, **argv )
{
 int doppio(), terzo(), cubo(), nonso();
 int( *funz )();
 int i = 1; /* serve a contare i parametri di main */

 while( i < argc ) {
   if( !strcmp( "doppio", argv[i]) )
      funz = doppio;
   else if( !strcmp( "terzo", argv[i] )
      funz = terzo;
   else if( !strcmp( "cubo", argv[i] )
      funz = cubo;
   else /* nessuno di questi */
      funz = nonso;
   printf( "%dn", (*funz)(atoi(argv[i+1])));
   i+=2;
 }
}

doppio( int i )
{
 return( i*2 );
}

terzo( int i )
{
 return( i/3 );
}

cubo( int i )
{
 return( i*i );
}

nonso( int i )
{
 return( 0 ); 
}

  
In questo  modo abbiamo  scritto un  programma che  in maniera
flessibile processa i  parametri passati  ed esegue  i calcoli
nell'ordine richiesto. Se non viene riconosciuta un'operazione
viene stampato uno zero.
Il  programma non esegue un controllo approfondito sui parame-
tri  passati e  sarebbero necessari  ulteriori test,  ma serve
allo scopo di dimostrare l'uso dei puntatori a funzione.

N.B. In  questo programma non  sono stati riportati  i file di
include che sono: stdio.h e string.h o stdlib.h

Questo tipo di  programmazione e'  molto utilizzato  in quelle
che vengono definite  macchine a  stati, ovvero  quei processi
informatici  in cui si passa da un livello di ordine superiore
ad  uno piu'  interno od  inferiore, ad  un altro  ancora piu'
interno  ritornando poi al livello precedente quando si incon-
tra un evento segnalatore.
Un  tipico esempio e' cio' che  avviene durante il parsing dei
programmi C: il programma parser del compilatore quando incon-
tra  il simbolo / passa ad un secondo stato in cui verifica se
il simbolo seguente e' un *. In tal caso si entra in un livel-
lo diverso, ovvero  si e' all'interno  di un commento  e se ne
fuoriesce solo quando si incontra un * seguito da un /.
Le alternative sono,  ad esempio che  il simbolo /  sia la ri-
chiesta  di una divisione, oppure una  parte di una stringa di
caratteri, cosi' come un *  all'interno  di  un  commento  non
faccia parte del terminatore */.

Identificando i vari stati come funzioni che eseguono svariati
compiti e'  evidente che una struttura di  tipo if then else o
una  switch case richiederebbe una complicazione eccessiva dal
momento che non si conosce  a  priori  l'evento  successivo  a
quello  attuale, mentre l'uso di  puntatori a funzione risulta
piu' flessibile.

Con questo si conclude la parte sulle funzioni ed i puntatori,
anche se l'argomento e'  molto vasto  e solo  la pratica  e la
analisi di  programmi  reali  puo'  portare  ad  una  migliore
comprensione degli argomenti.

Nei prossimi capitoli tratteremo dei  file,  i  vari  modi  di
gestire i flussi di dati con il C, nonche' l'uso di strutture,
union e tipi enumerali.