Le funzioni.

                                             di Roberto Porro

Fino a questo momento  abbiamo utilizzato semplici  programmi
costituiti da una sola funzione:  la main ed alcune  funzioni
di libreria, quali la printf,  scanf, ecc.  Nella stesura  di
programmi C  reali pero' si  articola il  programma in  varie
'funzioni'  che  hanno   il  compito   di  eseguire   compiti
specifici.    La   funzione  C   e'  un   blocco  di   codice
autocontenuto,  ovvero  completamente  definito,  che  riceve
dall'esterno i  parametri  in  ingresso  e  ritorna  dati  in
uscita, nonche' gli effetti  che essa stessa provoca  durante
la sua esecuzione.   Si  puo' immaginarla  come una  'scatola
nera' che interagisce col mondo esterno (il programma).  Come
e' stato  gia'  detto  in  precedenza  l'unica  funzione  che
obbligatoriamente deve essere presente  in un programma C  e'
la main,  poi si  possono aggiungere  tutte le  funzioni  che
necessitano, senza  limite di  numero.   Le funzioni  possono
richiamare  altre   funzioni,   possono   anche   richiamarsi
(ricorsione), possono modificare i parametri in ingresso,  ma
devono  ritornare  il  controllo  al  programma  main  quando
terminano.   (Solo  in caso  di  uscita per  errore  si  puo'
evitare di ritornare al main tramite, ad esempio, la 'exit').
Non esistono regole per l'utilizzo delle funzioni: non vi  e'
precedenza, profondita' di annidamento o regole  gerarchiche.
In pratica l'unica limitazione e' la memoria del sistema e lo
stack.   Ricordiamo che  ogni volta  che viene  chiamata  una
funzione il  sistema  salva  sullo stack  gli  indirizzi  del
chiamante, alloca spazio per i parametri e tutto cio'  occupa
memoria.

Dopo queste  premesse di  ordine generale  analizziamo  l'uso
delle funzioni.

/* Programma: FUNZIONE */
#include 

void main( void ); int funz1( int );

void main( void )
{
  int i;

  for( i = 0; i < 10; i++ )
      printf( "Chiamo funz1 volta n. %d\n", funz1( i ));
}

int funz1( int par1 )
{
  printf( "col valore %d ", par1 );
  return( par1+1 );
}

Il programma  FUNZIONE  utilizza  una  funzione  'funz1'  per
stampare una serie di valori.  La funz1 viene definita come:
   int funz1( int );
il che significa che funz1 ritorna  un valore di tipo int  ed
utilizza un parametro di tipo  int.  Il parametro passato  e'
l'indice del ciclo for.

La printf esegue funz1( i ) che stampa

  col valore

e visualizza par1, quindi ritorna par1+1.  Infine visualizza

  Chiamo funz1 volta n.

e stampa il valore di ritorno par1+1, ovvero i+1.

In pratica si ottiene un output tipo:

  col valore 0 Chiamo funz1 volta n. 1
  col valore 1 Chiamo funz1 volta n. 2
ecc...

funz1 potrebbe richiamare  una ipotetica  funz2 passando  non
piu' un  tipo  intero  ma  un  carattere  o  una  stringa  di
caratteri, oppure  funz2 potrebbe  seguire il  ciclo for  nel
main ed eseguire un altro compito e cosi' via.  Il numero dei
parametri e' limitato solo dalle necessita' del programma  e,
se non servono parametri in  ingresso o valori di ritorno  si
utilizzi il tipo 'void' per definire la funzione.

Ecco alcuni esempi di funzione:

void funz( void ); /* nessun valore in ingresso
                      nessun valore di ritorno */

int funz( int, int ); /* due interi in ingresso
                      ritorna un tipo int */

char funz( char* ); /* un array di caratteri in ingresso
                      ritona un tipo char */

int funz( char, int, double, char* ); /* quattro parametri
                      in ingresso: carattere, intero, double
                      e array di caratteri.
                      Ritorna un intero */

E' importante assicurarsi che  i parametri in ingresso  siano
coerenti con la definizione del  loro tipo per non  incorrere
in warning in  fase di compilazione  o a malfunzionamenti  in
fase di run-time.


- Funzioni ricorsive.

In C si  usa spesso  la ricorsione, ovvero  una funzione  che
richiama se stessa.

Es.:

main()
{
        printf( "main\n" );
        main();
}

Questo programma  si  richiama  all'infinito  e  deve  essere
interrotta dall'utente con l'appropriata sequenza di stop
(CTRL+D sul QL, CTRL+C su PC-DOS, ecc...).

- Funzioni, puntatori e valori di ritorno.

Vediamo un altro uso delle funzioni e dei parametri.

Una funzione ritorna  un solo  valore, per cui  se si  devono
modificare piu'  valori  all'interno della  funzione  e  tali
valori ci servono nel corpo della funzione chiamante dobbiamo
usare un piccolo espediente.

Analizziamo il seguente codice.

Supponiamo di dover modificare  il contenuto di due  variabi-
li: una stringa di caratteri c ed un intero d. Sia la stringa
che l'intero sono  noti all'interno della  main, ma non  sono
noti alla negac che li acquisice come parametri in  ingresso.
La negac  e'  definita  come  una  funzione  che  ritorna  un
puntatore a  carattere,  quindi  non  potrebbe  ritornare  il
valore di una variabile  di tipo intero, come  effettivamente
fa.  Dal momento che  abbiamo deciso di modificare il  valore
sia della stringa c che dell'intero d all'interno della negac
dobbiamo comportarci nel seguente modo:

1)negac ritorna un puntatore a carattere, ovvero  l'indirizzo
del primo  carattere  della  stringa;  2)negac  accetta  come
parametri un puntatore a carattere ed un puntatore ad intero,
ovvero gli indirizzi di una stringa e di un intero.

Passando c alla negac in realta' si fornisce il puntatore  ad
un indirizzo  di  memoria  che  e' la  locazione  in  cui  e'
contenuta la stringa c, cosi' come passando &d si fornisce il
puntatore all'indirizzo in cui  e' contenuta la variabile  di
tipo intero d.

A questo punto  nella funzione negac,  non sono contenute  le
immagini delle variabili c e d, ma i loro veri indirizzi, per
cui ogni  modifica effettuata  a queste  variabili altera  il
contenuto delle locazioni di  memoria delle due variabili  e,
quindi, permette di avere in  main i valori modificati  delle
stesse.

N.B.   i nomi  dei parametri  di negac,  ovvero f  e k,  sono
simbolici ed assumono l'indirizzo  di c e d,  per cui sono  a
tutti gli effetti equivalenti alle variabili dichiarate nella
main!!

#include 

char* negac( char*, int* );

main()
{
  char c[4];
  int d = 0;

  sprintf( c, "%s", "SI" );
  printf( "Inizio: c vale %s e d %d\n", c, d );
  negac( c, &d );
  printf( "Ritorno da negac: c vale %s e d %d\n", c, d );
}

char* negac( char* f, int *k )
{
  printf( "Ingresso di negac: c vale %s e d %d\n", f, *k );
  sprintf( f, "%s", "NO" );
  *k+=1;
}

Questo e' l'output del programma.

Inizio: c vale SI e d 0
Ingresso di negac: c vale SI e d 0
Ritorno da negac: c vale NO e d 1


Se avessimo scritto il programma nel seguente modo:

#include 

char* negac( char*, int );

main()
{
  char c[4];
  int d = 0;

  sprintf( c, "%s", "SI" );
  printf( "Inizio: c vale %s e d %d\n", c, d );
  negac( c, d );
  printf( "Ritorno da negac: c vale %s e d %d\n", c,d );
}

char* negac( char* f, int k )
{
  printf( "Ingresso di negac: c vale %s e d %d\n", f, k );
  sprintf( f, "%s", "NO" );
  k+=1;
}

l'output sarebbe stato:

Inizio: c vale SI e d 0
Ingresso di negac: c vale SI e d 0
Ritorno da negac: c vale NO e d 0

in quanto d viene passato  come parametro, giunti in negac  k
contiene l'immagine di d  ma la locazione  di memoria non  e'
quella di d,  k viene  incrementato, la  funzione ritorna  il
valore della stringa mentre viene perso il contenuto di k  e,
nella main, il valore di d resta invariato.

Bisogna prestare attenzione a  questo tipo di utilizzo  delle
funzioni e dei loro parametri  in quanto e' facile  incorrere
in errori o blocchi di sistema, poiche' si puo' andare a  far
riferimento ad aree di memoria inesistenti oppure  utilizzate
da altri programmi!!!!

ESEMPIO DI PROGRAMMA ERRATO!

#include 

char* negac( char*, int* );

main()
{
  char c[4];
  int d = 0;

  sprintf( c, "%s", "SI" );
  printf( "Inizio: c vale %s e d %d\n", c, d );
  negac( c, d ); /* NON SI PASSA L'INDIRIZZO DI D!!! */
  printf( "Ritorno da negac: c vale %s e d %d\n", c,d );
}

char* negac( char* f, int* k )
{
  printf( "Ingresso di negac: c vale %s e d %d\n", f, *k );
  sprintf( f, "%s", "NO" );
  *k+=1;
}

Il codice  precedente causa  warning  in compilazione  ed  un
grave errore di run-time  in quanto cerca  di accedere ad  un
dato che non puo' essere trovato  a causa del modo errato  in
cui e' stato passato il secondo parametro della funzione.

Nel prossimo  capitolo esamineremo  ancora alcune  proprieta'
delle funzioni e dei puntatori alle stesse.